Doku-Vollausbau: 62 Seiten (Projekt/Admin/Anwender/Referenz), Hook-Referenz mit Signaturen, Corporate-Screenshots, Coverage-Inventar
Deploy tibi-docs / build-and-deploy (push) Failing after 38s

- Einstieg: Schnellstart + Neues Projekt aus Starter-Skill, Was-ist-Tibi mit Backend/Website-Trennung
- Projekt: Sammlungen, Felder, Abfragen, Rechte, Hooks, Dateien, i18n, Suche, SSR, Pagebuilder, Jobs, Audit, Deploy, LLM, MCP
- Hook-Referenz: 66 Symbole mit Signatur + Beispiel + Fehlerfall (5 Kapitel)
- Admin: Ansichten, Editor, Medien, Exporte, Aktionen, Rechte, KI, Setup, Customizing
- Anwender: 11 Anleitungen im professionellen Du-Ton, 30 Corporate-Screenshots
- Referenz: Schemas, Token-Köpfe, Fehlerbilder, Shortcuts, OpenAPI-Platzhalter
- ADR-0001 bis 0003, Coverage-Skript (282 Items) + Map, Plan-Dateien
This commit is contained in:
apairon committed 2026-10-16 22:49:07 +00:00
1 parent acb1d32355
commit 1897bb50d9
101 files changed
+8978 -66

No files matched your search

+21 -13
View File
@@ -1,32 +1,40 @@
# AGENTS.md — Tibi Doku
Regeln für alle Agenten, die an dieser Doku oder an Tibi-Server / Tibi-Admin-Nova arbeiten.
Regeln für alle Agenten, die an dieser Doku, am Tibi-Server / tibi-admin oder an Projekten aus `tibi-svelte-starter` arbeiten.
## 0. Benennung und Blickwinkel
- Wir nennen es **tibi-server** und **tibi-admin** (Release Nova, Repo `tibi-admin-nova`). Alte `tibi-admin`-Repos werden ignoriert.
- Backend = tibi-server (API) + tibi-admin (Bedienoberfläche). Projekte sind eigenständige Websites/Shops/Intranet aus `tibi-svelte-starter`.
- Entwickler-Doku beschreibt **Projektnutzung** (klonen, konfigurieren, bauen), keine Tibi-Core-Entwicklung.
## 1. Grundsatz
- **Single Source:** Code-Nähe schlägt Doku-Kopie. Details stehen in `tibi-server/docs/` bzw. `tibi-admin-nova/docs/`, diese Seite rendert nur.
- **Single Source:** Code-Nähe schlägt Doku-Kopie. Details stehen in `tibi-server/docs/` bzw. `tibi-admin-nova/docs/` (Doku-Begriff: **tibi-admin**), diese Seite rendert nur. **Code schlägt Doku:** Bestandsdoku gilt als Behauptung, Quelle ist der Code (`internal/*`, `tibi-types/schemas`, `frontend/src/lib`, Tests, Test-Pläne).
- **Definition of Done:** Jede PR an `tibi-server` oder `tibi-admin-nova`, die API, Hooks, Collections, Permissions, Jobs, Realtime, LLM, Deploy oder UX ändert, aktualisiert auch die Docs — sonst ist die PR nicht fertig.
- **Sprache:** Deutsch, saubere Umlaute (ä ö ü ß), Du-Form für Anleitungen, kurze Sätze.
- **Sprache:** Deutsch, saubere Umlaute (ä ö ü ß), Du-Form, kurze Sätze. Ton: **per Du, aber professionell** — sachlich wie ein gutes Handbuch, niemals Babysprache. Keine belehrenden Binsen („wenn du nichts findest, verändere die Suche"), keine Verniedlichung („keine Angst"), keine erfundenen Floskeln. UI-Namen immer wörtlich aus `de.json` bzw. Screenshot. Anweisungen nennen den exakten Klickpfad; „Was-tun-wenn" nur für echte, am Code belegte Fälle.
## 2. Wo was hingehört (Diátaxis) — Stand: Neuaufbau
## 2. Wo was hingehört — Stand: Gerüst steht, Hüllen mit Arbeitsaufträgen
Aktuell existiert nur `einstieg/`. Weitere Bereiche werden sauber neu aufgebaut;
pro Bereich Seite anlegen, Sidebar-Gruppe in `astro.config.mjs` ergänzen.
Jede Hüllen-Seite enthält Abschnitt „Für spätere Agenten“ mit Quelle-der-Wahrheit (Code zuerst), Prüfschritten und Ziel-Struktur. Hüllen abarbeiten heißt: Code prüfen, Seite befüllen, `Verifiziert am <Commit> <Datum>` ergänzen.
| Ordner | Inhalt | Beispiel |
|---|---|---|
| `einstieg/` | Tutorials, Erste Schritte | Schnellstart, Begriffe |
| `server/` (geplant) | **SYNC — nicht von Hand editieren**, Quelle `../tibi-server/docs/` | Collections, Hooks |
| `nova/` (geplant) | Teils SYNC aus `../tibi-admin-nova/docs/`, plus Konzeptseite | Sammlungen, Export |
| `anwender/` (geplant) | Redakteurs-Anleitungen (Aufgaben, keine Konzepte) | Anmelden, Pflegen |
| `reference/` (geplant) | **Generiert — nie von Hand**, OpenAPI/Typen/Schema | openapi.md |
| `adr/` (geplant) | Architektur-Entscheide, Format siehe Vorlage im Git-Verlauf (`adr/0000-vorlage.md`) | Starlight-Entscheid |
| `einstieg/` | Tutorials, Erste Schritte (Projekt-Blick) | Schnellstart, Neues Projekt, Begriffe |
| `projekt/` | Projekt-Entwickler baut eigenständige Website (Sammlungen → Deploy) | Hooks, SSR, Suche |
| `server/` | **SYNC — nicht von Hand editieren**, Quelle `../tibi-server/docs/`; dazu Handseiten Betrieb | Konfiguration, Sicherheit |
| `admin/` | Teils SYNC aus `../tibi-admin-nova/docs/`, plus Handseiten | Konzept, Editor, Exporte |
| `anwender/` | Redakteurs-Anleitungen im Verwaltungs-Backend (Aufgaben, keine Konzepte) | Anmelden, Medien |
| `reference/` | Teils **generiert — nie von Hand** (openapi), teils Code-Stand-Tabellen | Hook-Kontexte, Token-Köpfe |
| `adr/` | Architektur-Entscheide, Format siehe `adr/0000-vorlage.md` | Code-schlägt-Doku |
## 3. Neue Seite anlegen
- Datei `src/content/docs/<bereich>/<slug>.md` mit Frontmatter `title` + `description`.
- Sidebar: jede Gruppe explizit in `astro.config.mjs` eintragen (beim Neuaufbau pro Bereich ergänzen; `autogenerate` erst nutzen, sobald der Ordner gepflegt befüllt ist).
- Bilder nach `src/assets/`, max. 200 KB, deutsche Alt-Texte.
- Screenshots für Anwender-Seiten: nur Mock-Projekt `corporate` (nie `t1`), Ablauf unter `plans/t-shots-arbeitsauftrag.md`.
- Vollständigkeit: neue Fähigkeit braucht genau eine Dokustelle — Tabelle in `plans/doku-ausbau.md` („Vollständigkeit") miterweitern.
## 4. Sync-Workflow
@@ -36,7 +44,7 @@ npm run sync
npm run build # muss fehlerfrei durchlaufen
```
- `server/uebersicht.md` und `nova/konzept.md` sind Handarbeit und werden vom Sync nie überschrieben (`keep`) — beim Neuaufbau zuerst diese beiden Seiten neu anlegen, dann syncen.
- `server/uebersicht.md` und `admin/konzept.md` sind Handarbeit und werden vom Sync nie überschrieben (`keep`) — Sync-Ziel für Admin ist `src/content/docs/admin/` (Repo bleibt `tibi-admin-nova`).
- Sync-Artefakte werden mitcommittet (statischer Build braucht sie).
## 5. Verbote
+88 -1
View File
@@ -17,7 +17,7 @@ export default defineConfig({
integrations: [
starlight({
title: 'Tibi Doku',
description: 'Entwickler- und Anwender-Dokumentation für Tibi-Server und Tibi-Admin-Nova',
description: 'Entwickler- und Anwender-Dokumentation für Tibi-Server und Tibi-Admin',
defaultLocale: 'root',
locales: {
root: { label: 'Deutsch', lang: 'de' },
@@ -42,6 +42,93 @@ export default defineConfig({
{ label: 'Was ist Tibi?', slug: 'einstieg/was-ist-tibi' },
{ label: 'Schnellstart', slug: 'einstieg/schnellstart' },
{ label: 'Begriffe', slug: 'einstieg/begriffe' },
{ label: 'Neues Projekt', slug: 'einstieg/neues-projekt' },
],
},
{
label: 'Server',
items: [
{ label: 'Übersicht', slug: 'server/uebersicht' },
{ label: 'Konfiguration', slug: 'server/konfiguration' },
{ label: 'Betrieb', slug: 'server/betrieb' },
{ label: 'Sicherheit', slug: 'server/sicherheit' },
{ label: 'Schnittstellen', slug: 'server/schnittstellen' },
],
},
{
label: 'Projekt bauen',
items: [
{ label: 'Sammlungen', slug: 'projekt/sammlungen-anlegen' },
{ label: 'Felder und Widgets', slug: 'projekt/felder-widgets' },
{ label: 'Lesen und suchen', slug: 'projekt/abfragen-lesen' },
{ label: 'Rechte und Tokens', slug: 'projekt/rechte-tokens' },
{ label: 'Hooks', slug: 'projekt/hooks-schreiben' },
{ label: 'Dateien und Bilder', slug: 'projekt/dateien-bilder' },
{ label: 'Navigation und Sprachen', slug: 'projekt/navigation-i18n' },
{ label: 'Suche', slug: 'projekt/suche' },
{ label: 'Pagebuilder-Anbindung', slug: 'projekt/pagebuilder-dev' },
{ label: 'Website mit SSR', slug: 'projekt/ssr-website' },
{ label: 'Jobs und Aktionen', slug: 'projekt/jobs-actions' },
{ label: 'LLM nutzen', slug: 'projekt/llm-nutzen' },
{ label: 'MCP nutzen', slug: 'projekt/mcp-nutzen' },
{ label: 'Sicherung', slug: 'projekt/audit-backup' },
{ label: 'Veröffentlichen', slug: 'projekt/deploy' },
],
},
{
label: 'Admin',
items: [
{ label: 'Konzept', slug: 'admin/konzept' },
{ label: 'Einrichten', slug: 'admin/setup' },
{ label: 'Anpassen', slug: 'admin/customizing' },
{ label: 'Ansichten und Listen', slug: 'admin/ansichten-listen' },
{ label: 'Eintrags-Editor', slug: 'admin/eintrags-editor' },
{ label: 'Medien', slug: 'admin/medien' },
{ label: 'Exporte', slug: 'admin/exporte' },
{ label: 'Aktionen', slug: 'admin/aktionen' },
{ label: 'Benutzer und Rechte', slug: 'admin/benutzer-rechte' },
{ label: 'KI-Funktionen', slug: 'admin/ki-funktionen' },
],
},
{
label: 'Anwender',
items: [
{ label: 'Anmelden', slug: 'anwender/anmelden' },
{ label: 'Zurechtfinden', slug: 'anwender/orientierung' },
{ label: 'Lesen, suchen, filtern', slug: 'anwender/lesen-suchen-filtern' },
{ label: 'Einträge bearbeiten', slug: 'anwender/eintrag-bearbeiten' },
{ label: 'Bilder und Dateien', slug: 'anwender/medien' },
{ label: 'Seiten bauen', slug: 'anwender/seiten-bauen' },
{ label: 'Benutzer und Teams', slug: 'anwender/benutzer-teams' },
{ label: 'Exportieren', slug: 'anwender/exportieren' },
{ label: 'Sichern', slug: 'anwender/sichern' },
{ label: 'Hilfe', slug: 'anwender/hilfe' },
{ label: 'KI nutzen', slug: 'anwender/ki-nutzen' },
],
},
{
label: 'Referenz',
items: [
{ label: 'OpenAPI', slug: 'reference/openapi' },
{ label: 'Schemas', slug: 'reference/schemas' },
{ label: 'Hook-Kontexte', slug: 'reference/hook-kontexte' },
{ label: 'Hook-Referenz: Daten', slug: 'reference/hooks-daten' },
{ label: 'Hook-Referenz: Netze und Nachrichten', slug: 'reference/hooks-netze' },
{ label: 'Hook-Referenz: Sicherheit und Identität', slug: 'reference/hooks-sicherheit' },
{ label: 'Hook-Referenz: Formate und Diagnose', slug: 'reference/hooks-meta' },
{ label: 'Hook-Referenz: Templates und Medien', slug: 'reference/hooks-medien' },
{ label: 'Token-Köpfe', slug: 'reference/token-koepfe' },
{ label: 'Fehlerbilder', slug: 'reference/fehlercodes' },
{ label: 'Tastatur und Schnellsuche', slug: 'reference/tastatur-kurzbefehle' },
],
},
{
label: 'ADR',
items: [
{ label: 'Vorlage', slug: 'adr/0000-vorlage' },
{ label: 'Starlight', slug: 'adr/0001-starlight' },
{ label: 'Code schlägt Doku', slug: 'adr/0002-code-schlaegt-doku' },
{ label: 'Selbstläufer-Regel', slug: 'adr/0003-selbstlaeufer' },
],
},
],
+1 -1
View File
@@ -16,7 +16,7 @@ services:
XDG_CONFIG_HOME: /tmp/.config
XDG_CACHE_HOME: /tmp/.cache
ASTRO_TELEMETRY_DISABLED: "1"
command: sh -c "npm install && npm run dev -- --host 0.0.0.0 --port 4321"
command: sh -c "rm -f .astro/dev.json && npm install && npm run dev -- --host 0.0.0.0 --port 4321"
expose:
- 4321
labels:
+2 -1
View File
@@ -10,7 +10,8 @@
"preview": "astro preview",
"astro": "astro",
"sync": "node scripts/sync-server-docs.mjs",
"sync:openapi": "node scripts/sync-openapi.mjs"
"sync:openapi": "node scripts/sync-openapi.mjs",
"coverage": "node scripts/coverage-inventory.mjs"
},
"dependencies": {
"@astrojs/starlight": "^0.42.6",
+346
View File
@@ -0,0 +1,346 @@
{
"_kommentar": "Start-Mapping Routen/Contexts/Tools/Schemas/Komponenten/Testplaene auf Doku-Slugs. Unvollstaendig by design, Rest fuellt der Orga-Agent. Keys muessen exakt den Inventar-Items aus scripts/coverage-inventory.mjs entsprechen.",
"routes": {
"/_/:project/:collection": "projekt/abfragen-lesen",
"/_/:project/:collection/:id": "projekt/abfragen-lesen",
"/_/:project/:collection/_count": "projekt/abfragen-lesen",
"/_/:project/:collection/_facet": "projekt/suche",
"/_/:project/:collection/_queue": "projekt/suche",
"/_/:project/_actions/:action": "projekt/jobs-actions",
"/_/:project/_/assets/:name/*path": "projekt/dateien-bilder",
"/_openapi/:project": "reference/openapi",
"/audit": "projekt/audit-backup",
"/llm/chat": "projekt/llm-nutzen",
"/mcp": "projekt/mcp-nutzen",
"/token/_scopes": "projekt/rechte-tokens",
"/version": "server/schnittstellen",
"_/:project/_/admin/backup": "projekt/audit-backup",
"_/:project/_/admin/snapshots": "projekt/audit-backup",
"/_/:project/:collection/:id/:field": "projekt/abfragen-lesen",
"/_/:project/:collection/:id/:field/*path": "projekt/abfragen-lesen",
"/llm/providers": "projekt/llm-nutzen",
"_/:project/_/admin/backup/:restoreToken": "projekt/audit-backup",
"_/:project/_/admin/backup/analyze-download": "server/schnittstellen",
"_/:project/_/admin/backup/download/:file": "projekt/audit-backup",
"_/:project/_/admin/embedding/status": "server/betrieb",
"_/:project/_/admin/regenerate-search": "server/betrieb",
"_/:project/_/admin/reload": "server/betrieb",
"_/:project/_/admin/search-worker/status": "server/betrieb",
"_/:project/_/admin/snapshots/:id": "projekt/audit-backup",
"_/:project/_/admin/snapshots/:id/download": "projekt/audit-backup",
"_/:project/_/admin/snapshots/:id/restore": "projekt/audit-backup",
"_/:project/_/admin/snapshots/configs": "projekt/audit-backup",
"_/:project/_/admin/snapshots/configs/:name/trigger": "projekt/audit-backup",
"_/:project/_/admin/snapshots/download/:file": "projekt/audit-backup",
"_/:project/_/admin/snapshots/prune": "projekt/audit-backup",
"_/:project/_/admin/snapshots/stats": "projekt/audit-backup",
"/*path": "server/sicherheit",
"/embedding/status": "server/schnittstellen",
"/shutdown": "server/schnittstellen"
},
"contexts": {
"base64.decode": "reference/hooks-meta",
"base64.encode": "reference/hooks-meta",
"bcrypt.check": "reference/hooks-sicherheit",
"bcrypt.hash": "reference/hooks-sicherheit",
"cache.clear": "reference/hooks-daten",
"cache.count": "reference/hooks-daten",
"cache.delete": "reference/hooks-daten",
"cache.get": "reference/hooks-daten",
"cache.has": "reference/hooks-daten",
"cache.set": "reference/hooks-daten",
"channel.send": "reference/hooks-netze",
"channel.subscribe": "reference/hooks-netze",
"charset.decode": "reference/hooks-meta",
"charset.iso8859ToUtf8": "reference/hooks-meta",
"config.namespace": "reference/hooks-sicherheit",
"config.project": "reference/hooks-sicherheit",
"config.server": "reference/hooks-sicherheit",
"cookie.get": "reference/hooks-sicherheit",
"cookie.set": "reference/hooks-sicherheit",
"crypto.md5": "reference/hooks-sicherheit",
"crypto.sha256": "reference/hooks-sicherheit",
"db.count": "reference/hooks-daten",
"db.create": "reference/hooks-daten",
"db.delete": "reference/hooks-daten",
"db.deleteMany": "reference/hooks-daten",
"db.find": "reference/hooks-daten",
"db.update": "reference/hooks-daten",
"db.updateMany": "reference/hooks-daten",
"debug.dump": "reference/hooks-meta",
"debug.sentryTraceId": "reference/hooks-meta",
"debug.sleep": "reference/hooks-meta",
"exec.command": "reference/hooks-sicherheit",
"fs.configDir": "reference/hooks-daten",
"fs.copy": "reference/hooks-daten",
"fs.mkDir": "reference/hooks-daten",
"fs.readDir": "reference/hooks-daten",
"fs.readFile": "reference/hooks-daten",
"fs.remove": "reference/hooks-daten",
"fs.stat": "reference/hooks-daten",
"fs.writeFile": "reference/hooks-daten",
"http.fetch": "reference/hooks-netze",
"http.fetchStream": "reference/hooks-netze",
"image.filter": "reference/hooks-medien",
"json.parse": "reference/hooks-meta",
"json.stringify": "reference/hooks-meta",
"jwt.create": "reference/hooks-sicherheit",
"jwt.parse": "reference/hooks-sicherheit",
"pdf.cpu": "reference/hooks-medien",
"pdf.fromHTML": "reference/hooks-medien",
"ratelimit.backoffCheck": "reference/hooks-netze",
"ratelimit.backoffRecord": "reference/hooks-netze",
"ratelimit.backoffReset": "reference/hooks-netze",
"ratelimit.windowCheck": "reference/hooks-netze",
"ratelimit.windowCount": "reference/hooks-netze",
"ratelimit.windowReset": "reference/hooks-netze",
"request": "reference/hooks-daten",
"response.flush": "reference/hooks-daten",
"response.header": "reference/hooks-daten",
"response.write": "reference/hooks-daten",
"response.writeHeader": "reference/hooks-daten",
"response.writeSSE": "reference/hooks-daten",
"smtp.sendMail": "reference/hooks-netze",
"tpl.execute": "reference/hooks-medien",
"user.auth": "reference/hooks-sicherheit",
"xml.create": "reference/hooks-meta",
"xml.parse": "reference/hooks-meta"
},
"tools": {
"action_call": "projekt/mcp-nutzen",
"actions_list": "projekt/mcp-nutzen",
"collections_list": "projekt/mcp-nutzen",
"document_get": "projekt/mcp-nutzen",
"documents_list": "projekt/mcp-nutzen",
"action_describe": "projekt/mcp-nutzen",
"agent_guide_get": "projekt/mcp-nutzen",
"asset_read": "projekt/mcp-nutzen",
"assets_list": "projekt/mcp-nutzen",
"audit_query": "projekt/mcp-nutzen",
"backup_create": "projekt/mcp-nutzen",
"collection_describe": "projekt/mcp-nutzen",
"document_create": "projekt/mcp-nutzen",
"document_delete": "projekt/mcp-nutzen",
"document_update": "projekt/mcp-nutzen",
"documents_count": "projekt/mcp-nutzen",
"documents_facet": "projekt/mcp-nutzen",
"file_read": "projekt/mcp-nutzen",
"jobs_list": "projekt/mcp-nutzen",
"openapi_get": "projekt/mcp-nutzen",
"org_get": "projekt/mcp-nutzen",
"orgs_list": "projekt/mcp-nutzen",
"project_get": "projekt/mcp-nutzen",
"project_reload": "projekt/mcp-nutzen",
"projects_list": "projekt/mcp-nutzen",
"queue_status": "projekt/mcp-nutzen",
"regenerate_search": "projekt/mcp-nutzen",
"snapshots_list": "projekt/mcp-nutzen",
"system_meta_get": "projekt/mcp-nutzen",
"team_get": "projekt/mcp-nutzen",
"teams_list": "projekt/mcp-nutzen",
"user_create": "projekt/mcp-nutzen",
"user_delete": "projekt/mcp-nutzen",
"user_get": "projekt/mcp-nutzen",
"user_update": "projekt/mcp-nutzen",
"users_list": "projekt/mcp-nutzen"
},
"schemas": {
"action.schema.json": "projekt/jobs-actions",
"collection.schema.json": "reference/schemas",
"field.schema.json": "projekt/felder-widgets",
"hooks.schema.json": "projekt/hooks-schreiben",
"job.schema.json": "projekt/jobs-actions",
"permissions.schema.json": "projekt/rechte-tokens",
"project.schema.json": "einstieg/neues-projekt",
"asset.schema.json": "reference/schemas",
"collection-meta.schema.json": "reference/schemas",
"defs.schema.json": "reference/schemas",
"field-list.schema.json": "reference/schemas",
"field-meta.schema.json": "reference/schemas",
"image-filter.schema.json": "reference/schemas",
"openapi.schema.json": "reference/schemas",
"projections.schema.json": "reference/schemas",
"snapshot.schema.json": "reference/schemas"
},
"components": {
"lib/components/ActionView": "admin/aktionen",
"lib/components/BackupView": "projekt/audit-backup",
"lib/components/CollectionView": "admin/ansichten-listen",
"lib/components/EntryView": "admin/eintrags-editor",
"lib/components/entry/widgets/pagebuilder/PagebuilderOverlay": "projekt/pagebuilder-dev",
"lib/components/admin/AdminHub": "admin/benutzer-rechte",
"lib/components/admin/OrgAssignmentSection": "admin/benutzer-rechte",
"lib/components/admin/OrgCreateDrawer": "admin/benutzer-rechte",
"lib/components/admin/OrgDetailDrawer": "admin/benutzer-rechte",
"lib/components/admin/OrgTab": "admin/benutzer-rechte",
"lib/components/admin/PermissionChipsInput": "admin/benutzer-rechte",
"lib/components/admin/ProjectAdminView": "admin/benutzer-rechte",
"lib/components/admin/ProjectCreateDrawer": "admin/benutzer-rechte",
"lib/components/admin/ProjectDetailDrawer": "admin/benutzer-rechte",
"lib/components/admin/ProjectRelationRow": "admin/benutzer-rechte",
"lib/components/admin/RoleSection": "admin/benutzer-rechte",
"lib/components/admin/TeamAssignmentSection": "admin/benutzer-rechte",
"lib/components/admin/TeamCreateDrawer": "admin/benutzer-rechte",
"lib/components/admin/TeamDetailDrawer": "admin/benutzer-rechte",
"lib/components/admin/TeamTab": "admin/benutzer-rechte",
"lib/components/admin/UserCreateDrawer": "admin/benutzer-rechte",
"lib/components/admin/UserDetailDrawer": "admin/benutzer-rechte",
"lib/components/admin/UserTab": "admin/benutzer-rechte",
"lib/components/ai/AiActionDivider": "admin/ki-funktionen",
"lib/components/ai/AiButton": "admin/ki-funktionen",
"lib/components/ai/AiChat": "admin/ki-funktionen",
"lib/components/ai/AiConversationList": "admin/ki-funktionen",
"lib/components/ai/AiDebugContext": "admin/ki-funktionen",
"lib/components/ai/AiHighlight": "admin/ki-funktionen",
"lib/components/ai/AiMessage": "admin/ki-funktionen",
"lib/components/ai/AiSummaryDivider": "admin/ki-funktionen",
"lib/components/ai/AiToolInspector": "admin/ki-funktionen",
"lib/components/ai/AiToolStatus": "admin/ki-funktionen",
"lib/components/ai/AiVisionOverlay": "admin/ki-funktionen",
"lib/components/entry/widgets/pagebuilder/BlueprintCard": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/pagebuilder/PagebuilderOverviewOverlay": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/pagebuilder/PagebuilderScreenshotHost": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/pagebuilder/PagebuilderThumbnail": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/pagebuilder/PagebuilderViewportControl": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/pagebuilder/PagebuilderWidget": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/pagebuilder/RowPreview": "projekt/pagebuilder-dev",
"lib/components/entry/widgets/BooleanWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/ChipArrayWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/DateWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/FileArrayWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/FileWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/ForeignFileWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/ForeignKeyChipArrayWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/ForeignKeyWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/ImageEditor": "admin/eintrags-editor",
"lib/components/entry/widgets/JsonWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/NavigationTreeField": "admin/eintrags-editor",
"lib/components/entry/widgets/NumberWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/ObjectArrayField": "admin/eintrags-editor",
"lib/components/entry/widgets/ObjectField": "admin/eintrags-editor",
"lib/components/entry/widgets/ObjectNavigationCard": "admin/eintrags-editor",
"lib/components/entry/widgets/RichtextWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/SelectWidget": "admin/eintrags-editor",
"lib/components/entry/widgets/TextWidget": "admin/eintrags-editor",
"lib/components/entry/DiffDialog": "admin/eintrags-editor",
"lib/components/entry/DrillDownBreadcrumb": "admin/eintrags-editor",
"lib/components/entry/EntryJsonOverlay": "admin/eintrags-editor",
"lib/components/entry/FieldRenderer": "admin/eintrags-editor",
"lib/components/entry/FieldSearch": "admin/eintrags-editor",
"lib/components/entry/ForeignKeyDrawer": "admin/eintrags-editor",
"lib/components/entry/I18nFieldWrapper": "admin/eintrags-editor",
"lib/components/entry/I18nLanguageTabs": "admin/eintrags-editor",
"lib/components/entry/InlineEntryForm": "admin/eintrags-editor",
"lib/components/entry/LinkedEntryEditorDrawer": "admin/eintrags-editor",
"lib/components/entry/PreviewElement": "admin/eintrags-editor",
"lib/components/entry/ReadonlyFieldDisplay": "admin/eintrags-editor",
"lib/components/entry/ReferencedBy": "admin/eintrags-editor",
"lib/components/entry/TranslationBar": "admin/eintrags-editor",
"lib/components/entry/WidgetDispatcher": "admin/eintrags-editor",
"lib/components/token/TokenAdminTab": "admin/benutzer-rechte",
"lib/components/token/TokenCreatedDialog": "admin/benutzer-rechte",
"lib/components/token/TokenFormDrawer": "admin/benutzer-rechte",
"lib/components/token/TokenList": "admin/benutzer-rechte",
"lib/components/token/TokenScopePicker": "admin/benutzer-rechte",
"lib/components/token/TokenSettings": "admin/benutzer-rechte",
"lib/components/ui/Avatar": "admin/ansichten-listen",
"lib/components/ui/Badge": "admin/ansichten-listen",
"lib/components/ui/Breadcrumb": "admin/ansichten-listen",
"lib/components/ui/Chip": "admin/ansichten-listen",
"lib/components/ui/CommandPalette": "admin/ansichten-listen",
"lib/components/ui/ConfirmDialog": "admin/ansichten-listen",
"lib/components/ui/DrawerBreadcrumb": "admin/ansichten-listen",
"lib/components/ui/ExportDialog": "admin/ansichten-listen",
"lib/components/ui/FieldValuePreview": "admin/ansichten-listen",
"lib/components/ui/FilterPopover": "admin/ansichten-listen",
"lib/components/ui/ForeignKeySelect": "admin/ansichten-listen",
"lib/components/ui/FormCard": "admin/ansichten-listen",
"lib/components/ui/FormField": "admin/ansichten-listen",
"lib/components/ui/Icon": "admin/ansichten-listen",
"lib/components/ui/KbdBadge": "admin/ansichten-listen",
"lib/components/ui/KeyboardShortcuts": "admin/ansichten-listen",
"lib/components/ui/LoadingState": "admin/ansichten-listen",
"lib/components/ui/MediaLightbox": "admin/ansichten-listen",
"lib/components/ui/NovaAiPill": "admin/ansichten-listen",
"lib/components/ui/NovaAiStreamContent": "admin/ansichten-listen",
"lib/components/ui/OptimizedImage": "admin/ansichten-listen",
"lib/components/ui/Pagination": "admin/ansichten-listen",
"lib/components/ui/PlaceholderCover": "admin/ansichten-listen",
"lib/components/ui/PreviewMetaItem": "admin/ansichten-listen",
"lib/components/ui/PreviewTitleBadge": "admin/ansichten-listen",
"lib/components/ui/RecordPreviewCard": "admin/ansichten-listen",
"lib/components/ui/RichTooltip": "admin/ansichten-listen",
"lib/components/ui/SaveActionButton": "admin/ansichten-listen",
"lib/components/ui/SearchIndexBuildNotice": "admin/ansichten-listen",
"lib/components/ui/SearchInput": "admin/ansichten-listen",
"lib/components/ui/SearchScoreIndicator": "admin/ansichten-listen",
"lib/components/ui/Section": "admin/ansichten-listen",
"lib/components/ui/SessionTimeoutWarning": "admin/ansichten-listen",
"lib/components/ui/Skeleton": "admin/ansichten-listen",
"lib/components/ui/SkeletonCard": "admin/ansichten-listen",
"lib/components/ui/SlideOverDrawer": "admin/ansichten-listen",
"lib/components/ui/SortPopover": "admin/ansichten-listen",
"lib/components/ui/Spinner": "admin/ansichten-listen",
"lib/components/ui/StructuredPreviewContent": "admin/ansichten-listen",
"lib/components/ui/ThemeToggle": "admin/ansichten-listen",
"lib/components/ui/Toast": "admin/ansichten-listen",
"lib/components/ui/UserList": "admin/ansichten-listen",
"lib/components/ui/ValidationBadge": "admin/ansichten-listen",
"lib/components/ui/VideoPlayer": "admin/ansichten-listen",
"lib/components/ui/VideoPreview": "admin/ansichten-listen",
"lib/components/ui/VoiceFilterTextInput": "admin/ansichten-listen",
"lib/components/ui/VoiceRecorderButton": "admin/ansichten-listen",
"lib/components/ui/VoiceTextInputControl": "admin/ansichten-listen",
"App": "admin/konzept",
"lib/components/ActionMappedFieldList": "admin/aktionen",
"lib/components/ActionMethodScaffold": "admin/aktionen",
"lib/components/ActionOutputMapping": "admin/aktionen",
"lib/components/ActionProgressMapping": "admin/aktionen",
"lib/components/AdminLayout": "admin/konzept",
"lib/components/AuditDiff": "projekt/audit-backup",
"lib/components/AuditHistoryPanel": "projekt/audit-backup",
"lib/components/AuditRestoreDialog": "projekt/audit-backup",
"lib/components/AuditSidebarHistory": "projekt/audit-backup",
"lib/components/AuditView": "projekt/audit-backup",
"lib/components/BulkEditPanel": "admin/ansichten-listen",
"lib/components/CollectionCards": "admin/ansichten-listen",
"lib/components/CollectionList": "admin/ansichten-listen",
"lib/components/CollectionTable": "admin/ansichten-listen",
"lib/components/FileUploadQueue": "admin/medien",
"lib/components/LoginPage": "anwender/anmelden",
"lib/components/MediaGrid": "admin/medien",
"lib/components/MediaGridTile": "admin/medien",
"lib/components/NavigationCollectionView": "admin/ansichten-listen",
"lib/components/ProfilePage": "anwender/benutzer-teams",
"lib/components/ProjectList": "admin/konzept",
"lib/components/SnapshotView": "projekt/audit-backup",
"__ignore": [
"lib/mock/*",
"*.test",
"lib/i18n/locales/*",
"*.d",
"graphify-out/*"
]
},
"testplans": {
"05-collection-view.md": "admin/ansichten-listen",
"37-pagebuilder.md": "projekt/pagebuilder-dev",
"40-actions.md": "admin/aktionen",
"43-backup-restore.md": "projekt/audit-backup",
"44-snapshots.md": "projekt/audit-backup",
"02-navigation-routing.md": "anwender/orientierung",
"07-entry-editor-advanced.md": "admin/eintrags-editor",
"11-foreign-keys.md": "admin/eintrags-editor",
"16-ai-integration.md": "admin/ki-funktionen",
"17-ai-tts.md": "admin/ki-funktionen",
"25-file-upload.md": "admin/medien",
"27-media-grid.md": "admin/medien",
"34-i18n-fields.md": "admin/eintrags-editor",
"35-entry-i18n.md": "admin/eintrags-editor",
"36-voice-input-fields.md": "admin/eintrags-editor",
"38-foreign-file.md": "admin/medien",
"39-preview-resolver.md": "admin/customizing",
"41-search-index-status.md": "projekt/suche",
"42-impersonation.md": "admin/benutzer-rechte"
}
}
+874
View File
@@ -0,0 +1,874 @@
{
"generatedAt": "2026-10-16T22:24:42.372Z",
"categories": {
"routes": {
"total": 36,
"mapped": 36,
"openCount": 0,
"openItems": []
},
"contexts": {
"total": 66,
"mapped": 66,
"openCount": 0,
"openItems": [],
"symbols": [
{
"symbol": "base64.decode",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_base64.go",
"hasArgs": true
},
{
"symbol": "base64.encode",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_base64.go",
"hasArgs": true
},
{
"symbol": "bcrypt.check",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_bcrypt.go",
"hasArgs": true
},
{
"symbol": "bcrypt.hash",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_bcrypt.go",
"hasArgs": true
},
{
"symbol": "cache.clear",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_cache.go",
"hasArgs": true
},
{
"symbol": "cache.count",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_cache.go",
"hasArgs": true
},
{
"symbol": "cache.delete",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_cache.go",
"hasArgs": true
},
{
"symbol": "cache.get",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_cache.go",
"hasArgs": true
},
{
"symbol": "cache.has",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_cache.go",
"hasArgs": true
},
{
"symbol": "cache.set",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_cache.go",
"hasArgs": true
},
{
"symbol": "channel.send",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_channel.go",
"hasArgs": true
},
{
"symbol": "channel.subscribe",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_channel.go",
"hasArgs": true
},
{
"symbol": "charset.decode",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_charset.go",
"hasArgs": true
},
{
"symbol": "charset.iso8859ToUtf8",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_charset.go",
"hasArgs": true
},
{
"symbol": "config.namespace",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_config.go",
"hasArgs": false
},
{
"symbol": "config.project",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_config.go",
"hasArgs": false
},
{
"symbol": "config.server",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_config.go",
"hasArgs": false
},
{
"symbol": "cookie.get",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_cookie.go",
"hasArgs": true
},
{
"symbol": "cookie.set",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_cookie.go",
"hasArgs": true
},
{
"symbol": "crypto.md5",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_crypto.go",
"hasArgs": true
},
{
"symbol": "crypto.sha256",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_crypto.go",
"hasArgs": true
},
{
"symbol": "db.count",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "db.create",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "db.delete",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "db.deleteMany",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "db.find",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "db.update",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "db.updateMany",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_db.go",
"hasArgs": true
},
{
"symbol": "debug.dump",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_debug.go",
"hasArgs": true
},
{
"symbol": "debug.sentryTraceId",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_debug.go",
"hasArgs": false
},
{
"symbol": "debug.sleep",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_debug.go",
"hasArgs": true
},
{
"symbol": "exec.command",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_exec.go",
"hasArgs": true
},
{
"symbol": "fs.configDir",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": false
},
{
"symbol": "fs.copy",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "fs.mkDir",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "fs.readDir",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "fs.readFile",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "fs.remove",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "fs.stat",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "fs.writeFile",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_fs.go",
"hasArgs": true
},
{
"symbol": "http.fetch",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_http.go",
"hasArgs": true
},
{
"symbol": "http.fetchStream",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_http.go",
"hasArgs": true
},
{
"symbol": "image.filter",
"page": "reference/hooks-medien",
"stufe": 4,
"goFile": "context_image.go",
"hasArgs": true
},
{
"symbol": "json.parse",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_json.go",
"hasArgs": true
},
{
"symbol": "json.stringify",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_json.go",
"hasArgs": true
},
{
"symbol": "jwt.create",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_jwt.go",
"hasArgs": true
},
{
"symbol": "jwt.parse",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_jwt.go",
"hasArgs": true
},
{
"symbol": "pdf.cpu",
"page": "reference/hooks-medien",
"stufe": 4,
"goFile": "context_pdf.go",
"hasArgs": false
},
{
"symbol": "pdf.fromHTML",
"page": "reference/hooks-medien",
"stufe": 4,
"goFile": "context_pdf.go",
"hasArgs": false
},
{
"symbol": "ratelimit.backoffCheck",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_ratelimit.go",
"hasArgs": true
},
{
"symbol": "ratelimit.backoffRecord",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_ratelimit.go",
"hasArgs": true
},
{
"symbol": "ratelimit.backoffReset",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_ratelimit.go",
"hasArgs": true
},
{
"symbol": "ratelimit.windowCheck",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_ratelimit.go",
"hasArgs": true
},
{
"symbol": "ratelimit.windowCount",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_ratelimit.go",
"hasArgs": true
},
{
"symbol": "ratelimit.windowReset",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_ratelimit.go",
"hasArgs": true
},
{
"symbol": "request",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_request.go",
"hasArgs": false
},
{
"symbol": "response.flush",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_response.go",
"hasArgs": false
},
{
"symbol": "response.header",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_response.go",
"hasArgs": false
},
{
"symbol": "response.write",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_response.go",
"hasArgs": true
},
{
"symbol": "response.writeHeader",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_response.go",
"hasArgs": true
},
{
"symbol": "response.writeSSE",
"page": "reference/hooks-daten",
"stufe": 4,
"goFile": "context_response.go",
"hasArgs": true
},
{
"symbol": "smtp.sendMail",
"page": "reference/hooks-netze",
"stufe": 4,
"goFile": "context_smtp.go",
"hasArgs": true
},
{
"symbol": "tpl.execute",
"page": "reference/hooks-medien",
"stufe": 4,
"goFile": "context_tpl.go",
"hasArgs": true
},
{
"symbol": "user.auth",
"page": "reference/hooks-sicherheit",
"stufe": 4,
"goFile": "context_user.go",
"hasArgs": false
},
{
"symbol": "xml.create",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_xml.go",
"hasArgs": true
},
{
"symbol": "xml.parse",
"page": "reference/hooks-meta",
"stufe": 4,
"goFile": "context_xml.go",
"hasArgs": true
}
]
},
"tools": {
"total": 36,
"mapped": 36,
"openCount": 0,
"openItems": [],
"symbols": [
{
"symbol": "action_call",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "action_describe",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "actions_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "agent_guide_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "asset_read",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "assets_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "audit_query",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "backup_create",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "collection_describe",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "collections_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "document_create",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "document_delete",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "document_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "document_update",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "documents_count",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "documents_facet",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "documents_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "file_read",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "jobs_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "openapi_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "org_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "orgs_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "project_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "project_reload",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "projects_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "queue_status",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "regenerate_search",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "snapshots_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "system_meta_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "team_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "teams_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "user_create",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "user_delete",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "user_get",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "user_update",
"page": "projekt/mcp-nutzen",
"stufe": 4
},
{
"symbol": "users_list",
"page": "projekt/mcp-nutzen",
"stufe": 4
}
]
},
"schemas": {
"total": 16,
"mapped": 16,
"openCount": 0,
"openItems": []
},
"components": {
"total": 151,
"mapped": 151,
"openCount": 0,
"openItems": []
},
"testplans": {
"total": 19,
"mapped": 19,
"openCount": 0,
"openItems": []
}
},
"depth": {
"routes": {
"total": 36,
"stufe4": 0,
"stufe3": 0,
"stufe2": 36,
"stufe1": 0
},
"contexts": {
"total": 66,
"stufe4": 66,
"stufe3": 0,
"stufe2": 0,
"stufe1": 0
},
"tools": {
"total": 36,
"stufe4": 36,
"stufe3": 0,
"stufe2": 0,
"stufe1": 0
},
"schemas": {
"total": 16,
"stufe4": 0,
"stufe3": 0,
"stufe2": 16,
"stufe1": 0
},
"components": {
"total": 151,
"stufe4": 0,
"stufe3": 0,
"stufe2": 151,
"stufe1": 0
},
"testplans": {
"total": 19,
"stufe4": 0,
"stufe3": 0,
"stufe2": 19,
"stufe1": 0
}
},
"schemasDetail": {
"action.schema.json": [
"audit",
"cors",
"hooks",
"meta",
"name",
"permissions"
],
"asset.schema.json": [
"name",
"path"
],
"collection-meta.schema.json": [
"agent",
"defaultSort",
"designGuide",
"exports",
"group",
"hide",
"i18n",
"imageUrl",
"label",
"muiIcon",
"openapi",
"pagebuilder",
"preview",
"query",
"sections",
"sidebar",
"singleton",
"subNavigation",
"viewHint"
],
"collection.schema.json": [
"audit",
"bulk",
"cors",
"defaultLanguage",
"fields",
"hiddenFields",
"hooks",
"imageFilter",
"indexes",
"meta",
"name",
"permissions",
"projections",
"queryLimits",
"readonlyFields",
"search",
"strictFields",
"upload",
"uploadPath"
],
"defs.schema.json": [],
"field-list.schema.json": [],
"field-meta.schema.json": [
"addElementLabel",
"choices",
"containerProps",
"defaultValue",
"dependsOn",
"downscale",
"drillDown",
"filter",
"foreign",
"helperText",
"hide",
"hideLabel",
"i18n",
"imageEditor",
"inputProps",
"label",
"openapi",
"pageSize",
"pagebuilder",
"position",
"preview",
"protected",
"schemaRef",
"section",
"widget"
],
"field.schema.json": [],
"hooks.schema.json": [
"audit",
"delete",
"get",
"post",
"put"
],
"image-filter.schema.json": [],
"job.schema.json": [
"cron",
"file",
"meta",
"timeout",
"type"
],
"openapi.schema.json": [
"delete",
"disabled",
"get",
"getOne",
"post",
"put"
],
"permissions.schema.json": [
"public",
"user"
],
"project.schema.json": [
"actions",
"admin",
"assets",
"audit",
"collections",
"cors",
"hook",
"jobs",
"legacy",
"meta",
"namespace",
"snapshotPath",
"snapshots",
"strictFields",
"upload"
],
"projections.schema.json": [],
"snapshot.schema.json": [
"collectionDedup",
"collections",
"description",
"enabled",
"includeMedia",
"mode",
"name",
"permissions",
"retention",
"schedule"
]
}
}
+167
View File
@@ -0,0 +1,167 @@
# Doku-Ausbau: Plan und Status
Lebendes Dokument. Der Orga-Agent (Agent Warpfuchs) aktualisiert Status, Sessions und Second-Opinion-Ergebnisse nach jedem Schritt. Stand: 2026-10-16.
## Regeln (kurz)
- Benennung: **tibi-server**, **tibi-admin** (Release Nova, Repo `tibi-admin-nova`). Projekte = eigenständige Websites/Shops/Intranet aus `tibi-svelte-starter`.
- Dev-Doku = Projektnutzung, keine Core-Entwicklung. Ton: Deutsch, Du-Form, kurze Sätze.
- Keine lange Prosa: Tabellen, Listen, Steps, Tabs, Code-Beispiele, Starlight-Komponenten.
- Code schlägt Doku: jede Aussage am Code verifiziert, Stempel `Verifiziert am <Commit> <Datum>`.
- `npm run build` in tibi-docs bleibt immer grün. Keine toten Links.
## Themenpakete
| Paket | Seiten | Agent | Status | Second Opinion | Abnahme |
|---|---|---|---|---|---|
| T-REF Referenz | schemas, hook-kontexte, token-köpfe, fehlercodes | general ses_ebb2308e0ffeybmgl7oG1Eyn0b | ✅ fertig | ✅ SO eingearbeitet (16 Schemas, 409/500-Legacy-Default, Login-Pfad) | Build + Links ✅ |
| T-PROJ-A Projekt Kern | sammlungen-anlegen, felder-widgets, abfragen-lesen, rechte-tokens | general ses_ebb2308d4ffev0H1z4mvNfsw5k | ✅ fertig | ✅ SO eingearbeitet (_count/_facet-Routen, required name+permissions, 25 Widgets) | Build + Links ✅ |
| T-PROJ-B Projekt Logik | hooks-schreiben, dateien-bilder, navigation-i18n, suche | general ses_ebb0d36b6ffeJ44pbErML0Z4X9 | ✅ fertig | ✅ SO eingearbeitet (SSR-Cache vs Dependencies, topK + YAML-rrf-Block) | Build + Links ✅ |
| T-PROJ-C Projekt Ship | ssr-website, jobs-actions, audit-backup, deploy | general ses_ebb0d36a7ffeY76nGYPhP0U5xR | ✅ fertig | ✅ SO eingearbeitet (cron-Pflicht korrigiert, Snapshot-Zitat + tags-Typo) | Build + Links ✅ |
| T-SERVER Server-Hand | konfiguration, betrieb, sicherheit, schnittstellen | general ses_ebb2308d3ffeCTZqqtzMszuDnH | ✅ fertig | ✅ SO eingearbeitet (MCP-Route fest, CORS-Merge, 429, kein Abo-Endpunkt) | Build + Links ✅ |
| T-ADMIN-A Admin I | ansichten-listen, eintrags-editor, medien, exporte | general ses_ebb0d36a5ffe0n1PbsS1E6V5HM | ✅ fertig | ✅ SO eingearbeitet (Custom-Export: db.loadAll/ctx.xlsx/contentType+blob, qm-Param) | Build + Links ✅ |
| T-ADMIN-B Admin II | aktionen, benutzer-rechte, ki-funktionen, setup, customizing | general ses_ebad2bf8affeZ9uV0HzJf2k357 | ✅ fertig | ✅ SO eingearbeitet (Test-Pläne 40/42 verlinkt, Widget-Enum klargestellt, toter Anker gefixt) | Build + Links ✅ |
| T-ANW-A Anwender I | anmelden, orientierung, lesen-suchen-filtern, eintrag-bearbeiten, medien | general ses_ebad2bf77ffeQm3uhj279Qrc09 | ✅ fertig | ✅ SO eingearbeitet (/ statt Strg+K, „Dateien hinzufügen", Tabelle statt Zeilen, Duplikat-Aside) | Build + Links ✅ |
| T-ANW-B Anwender II | seiten-bauen, benutzer-teams, exportieren, sichern, hilfe | general ses_ebad2bf75ffezS1KTRTQDO5GT5 | ✅ fertig | ✅ SO eingearbeitet (Snapshot-Reiter, „Exportieren", Benutzerverwaltung, echte Fehltexte) | Build + Links ✅ |
| T-ADMIN-B Admin II | aktionen, benutzer-rechte, ki-funktionen, setup, customizing | – | ⏳ offen | ⏳ | Build + Links |
| T-ANW-A Anwender I | anmelden, orientierung, lesen-suchen-filtern, eintrag-bearbeiten, medien | – | ⏳ offen | ⏳ | Build + Links |
| T-ANW-B Anwender II | seiten-bauen, benutzer-teams, exportieren, sichern, hilfe | – | ⏳ offen | ⏳ | Build + Links |
| T-SHOTS Screenshots | `src/assets/anwender/*` NUR aus Mock-Projekt `corporate` (nie `t1`!), Auftrag: `plans/t-shots-arbeitsauftrag.md` | general ses_ebac0c7feffed3DYtWznC2bqzJ | ✅ fertig (24 webp, <200 KB, Vision-Spotcheck ok) | ✅ Text-Abweichungen eingearbeitet (Live-Schalter, OK-Button, Reiter, Corporate-Blöcke) | Build + Links ✅ |
| T-PROJ-D Vollständigkeit | pagebuilder-dev, mcp-nutzen (36 Tools), llm-nutzen | general ses_ebac0c804ffe9vElWvaKTju4H9 | ✅ fertig | ✅ SO eingearbeitet (REST-only: sieben statt fünf) | Build + Links ✅ |
| T-FINISH Finish | index-Karten, uebersicht/konzept-Stempel, Voll-Linkcheck, AGENTS-Spiegelung | Orga (direkt) | ✅ fertig (v1) | — (Linkcheck: alle ok, 0 Hüllen, ADR-0003 angelegt) | Build 55 Seiten ✅ |
| T-PB Pagebuilder komplett | pagebuilder-dev + seiten-bauen: Overlay-Editor, Modi, Viewport, Overview/inline, drillDown | general ses_eb9584bafffekRtyPPBn7ORTeA | ✅ fertig | ✅ SO eingearbeitet (OK/blur-Pfad, <768-px-Hinweis) | Build + Links ✅ |
| T-TON Ton-Rewrite | alle 10 anwender-Seiten: professionell-per-du, UI-Namen aus de.json | general ses_eb95443a5ffe8IrJVmt1DcAReX | ✅ fertig | ✅ SO eingearbeitet („Bild hochladen", Sitzungs-Hinweis, Live-Qualifier, KI-Beschreibung, Browser-Zurück) | Build + Links ✅ |
| T-SHOTS-2 Pagebuilder-Shots | Overlay + Viewport (corporate), Einbau in seiten-bauen | general ses_eb9503e20ffetq8gZU0Ff5U06T | ✅ fertig (3 webp, Icon-Tooltip korrigiert) | ✅ Abweichung eingearbeitet | Build ✅ |
| T-COV Coverage-Inventar | Skript + Map: Routen, Kontexte, Tools, Schemas, Komponenten, Test-Pläne | general ses_eb9584ba6ffeUrQq7fowAHlPXO | ✅ fertig (282/282 gemappt, 0 OFFEN) | ⏳ Map v1 = Topic-Level bei ui/*, Verifikation folgt | `npm run coverage` grün |
Legende: ⏳ offen · 🔄 läuft (Session-ID) · 👀 Review/SO läuft · ✅ fertig.
## Vollständigkeit: Was ist wo abgedeckt (100-%-Ziel)
Jede Fähigkeit von tibi-server / tibi-admin braucht genau eine Dokustelle. Der Orga-Agent prüft diese Tabelle vor T-FINISH Zeile für Zeile.
### tibi-server (`internal/*`)
| Baustein | Dokustelle | Status |
|---|---|---|
| config (Server-Config, Env, Admin-Tokens) | server/konfiguration | ✅ |
| controller + dbapi + dbcrud + mongocrud (Routen, CRUD, Filter) | server/schnittstellen, projekt/abfragen-lesen | ✅ |
| hook (24 Kontexte) + Lifecycle-Stufen (get/post/put/delete/audit) | reference/hook-kontexte, projekt/hooks-schreiben | ✅ |
| realtime (Hub, SSE) | server/schnittstellen | ✅ |
| llm (chat, providers, Budgets) | projekt/llm-nutzen | ⏳ Hülle |
| mcp (36 Tools, Transport, Scopes) | projekt/mcp-nutzen (+ server/schnittstellen Überblick) | ⏳ Hülle |
| image (Filter) + upload | projekt/dateien-bilder | ✅ |
| ratelimit (Login-Backoff, 429) | server/sicherheit | ✅ |
| suexec + Pfad-Schalter | server/sicherheit | ✅ |
| audit (TTL, Lesen) | projekt/audit-backup | ✅ |
| jobs (Cron) + actions | projekt/jobs-actions | ✅ |
| snapshots + backup/restore | projekt/audit-backup | ✅ |
| search (Textindex, RRF-Hybrid, Regenerate) | projekt/suche (+ projekt/sammlungen-anlegen für Index-YAML) | ✅ |
| embedding + vectorindex | projekt/suche (Kurzüberblick) | ✅ |
| rankfusion (RRF-Formel) | projekt/suche | ✅ |
| permissions (public/user/token, Orgs/Teams-Matrix) | projekt/rechte-tokens | ✅ |
| filecleanup (automatisch, keine Config-Fläche) | kein eigener Artikel — Einzeiler in server/betrieb prüfen (T-FINISH) | ⏳ |
| cache (Hook-lokal) | projekt/hooks-schreiben (Kontext-Tabelle) | ✅ |
| mock, models, testsuite, utils, version, mgocrud.old (intern, keine Projekt-Fläche) | keine Doku nötig | — |
| SSR-Hooks + ssr-Collection + clear_cache | projekt/ssr-website | ✅ |
| CORS-Kaskade | server/sicherheit | ✅ |
| OpenAPI-Generator | reference/openapi + server/schnittstellen | ✅ |
| Deploy/Compose-Profile | projekt/deploy + server/betrieb | ✅ |
### tibi-admin (`frontend/src/...`)
| Baustein | Dokustelle | Status |
|---|---|---|
| Ansichten (Tabelle/Karten/Medien/Navigation) + Suche/Sort/Filter/Blättern | admin/ansichten-listen + anwender/lesen-suchen-filtern | ✅ |
| Editor + 25 Widgets + Fremdschlüssel + i18n | admin/eintrags-editor + anwender/eintrag-bearbeiten | ✅ |
| Medien (Grid, Upload, Editor, Lightbox) | admin/medien + anwender/medien | ✅ |
| Exporte (Listen + Custom-Skripte) | admin/exporte + anwender/exportieren | ✅ |
| Actions (Ausführen, Fortschritt) | admin/aktionen | ✅ |
| Benutzer/Teams/Orgs/Rollen/Tokens/Impersonation | admin/benutzer-rechte + anwender/benutzer-teams | ✅ |
| KI (Chat, Zusammenfassen, STT/TTS, Budgets) | admin/ki-funktionen | ✅ |
| Pagebuilder (Widget, Registry, CSS-Transform, Vorschau, Screenshots) | projekt/pagebuilder-dev + admin/medien (Thumbnail?) + anwender/seiten-bauen | ⏳ Hülle (dev), Rest ✅ |
| Setup (Docker, Mock, URLs) + Customizing (Views, Meta, Skripte) | admin/setup, admin/customizing | ✅ |
| CommandPalette, Shortcuts, Breadcrumb, Audit/history-UI, Backup/Snapshot-UI | verstreut in admin/* — T-FINISH prüft Erwähnung je Seite | ⏳ |
### tibi-types + Starter
| Baustein | Dokustelle | Status |
|---|---|---|
| 16 Schemas (project, collection, field, hooks, permissions, job, action, snapshot, ...) | reference/schemas + je Projekt-Seite | ✅ |
| Starter-Skill (Platzhalter, Registrierung, Pfad A/B) | einstieg/schnellstart + einstieg/neues-projekt | ✅ |
| Mock-Projekte (corporate/shop/blog/intranet/events/t1) | plans/t-shots-arbeitsauftrag.md (corporate für Shots) | ✅ |
## Wellen
- Welle 1: T-REF, T-PROJ-A, T-SERVER (Fundament, unabhängig voneinander).
- Welle 2: T-PROJ-B, T-PROJ-C, T-ADMIN-A.
- Welle 3: T-ADMIN-B, T-ANW-A, T-ANW-B.
- Danach: T-SHOTS, dann T-FINISH.
## Methode Vollständigkeit (v2 — messbar statt Gefühl)
Stufe 1 — Inventare (Skript `scripts/coverage-inventory.mjs`, Befehl `npm run coverage`): Routen aus `setup.go`, Hook-Kontexte aus `context_*.go`, MCP-Tools aus `tools_*.go`, Schema-Top-Level aus `tibi-types`, Admin-Komponenten (`*.svelte`), Test-Pläne (Liste). Jedes Item → genau eine Doku-Seite (`plans/coverage-map.json`). Unzugeordnete Items = Arbeitsliste mit Zahl.
Stufe 2 — Lücken-Pakete wie bisher (Generalist + Second Opinion), aber mit Inventar-Beleg.
Stufe 3 — Ton-Rewrite nach Stil-Guide (AGENTS.md §1), UI-Namen wörtlich aus `de.json`.
## Ausbau Tiefe (v3 — Tiefe statt Erwähnung, mit Second Opinion abgestimmt)
Befund: Das Inventar zählt Pakete statt Funktionen — „282/282" misst Tiefe grundsätzlich nicht. Neue Körnung: **Funktion**, neue Messlatte pro Item: `Erwähnung < Signatur < Signatur+Beispiel < Signatur+Beispiel+Fehler` (Ziel: Stufe 4 überall). Quelle fertig: `// args:`-Kommentare über jedem `Func*` in `context_*.go`.
Tiefenstufen-DoD: Referenz = Name + Signatur + Codeblock mit Symbol + Fehlerfall (skriptgeprüft). Anleitung = jeder H2 Schritt mit wörtlichem UI-Namen + prüfbarem Ergebnis. Anwender = jeder Schritt nennt UI-String wörtlich. Übersicht = jede Fähigkeit genau ein Link.
| Paket | Inhalt | Agent | Status | Second Opinion | Abnahme |
|---|---|---|---|---|---|
| P-DEPTH Messlatte | Map v2 Funktionskörnung + Depth-Mode im Skript (Gate vor allem Schreiben!) | general ses_eb9485d3cffeqGunIdpSLocnBq | ✅ fertig (66 Symbole, 0 auf Stufe 3/4 → ehrliche Null) | ✅ Orga-Entscheide (JS-signature zählt, context-Alias) eingebaut | `npm run coverage` mit Tiefenstufen ✅ |
| P-HOOKS-1 Daten | db, cache, request, response, fs (+ Referenz-Muster für alle) | general ses_eb9442455ffelLESMgDZZ40T4G | ✅ fertig (27 Symbole Stufe 4) | ✅ SO eingearbeitet (Zitat-Zeilen, configDir-Marker, Scanner per-###) | Build ✅ |
| P-HOOKS-2 Netze | http, smtp, channel, ratelimit | general ses_eb93e70f1fferm8NNuFeh6Ktn5 | ✅ fertig | ✅ SO-Spotcheck (smtp-Alias = Namensrest) | Stufe 4 ✅ |
| P-HOOKS-3 Sicherheit | jwt, user, bcrypt, crypto, cookie, exec, config | general ses_eb93e70e7ffedQM6Xqv9hH20oj | ✅ fertig | ✅ SO-Spotcheck (bcrypt-s, jwt-valid:false) | Stufe 4 ✅ |
| P-HOOKS-4 Medien | tpl, image, pdf | general ses_eb93e70e6ffepuNEqdv3N3uabP | ✅ fertig | ✅ SO (int64-Hinweis ergänzt) | Stufe 4 ✅ |
| P-HOOKS-5 Meta | base64, json, xml, charset, debug | general ses_eb93e70e5ffeuZXn6frVY3h2Do | ✅ fertig | ✅ SO-Spotcheck (dump/Kein-Fehlerfall ok) | Stufe 4 ✅ |
| P-MCP-D Tools-Tiefe | 36 Tools je Signatur+Beispiel+Fehler | general ses_eb93b206effe1eyg3y08aNDl7c | ✅ fertig (36/36 Stufe 4) | ✅ SO (9 REST-only, action_*-Kanon in mcp-nutzen, msg-vs-message präzisiert) | Build ✅ |
| P-LLM-D KI-Tiefe | Chat/Provider/Budget + ki-funktionen vertiefen | general ses_eb93435b4ffe2RV01zcGmhDqIb | ✅ fertig | ✅ SO (502-Config-Relativierung, Vision-Overlay, 4 fehlende Tools) | Build ✅ |
| P-UI-D Oberfläche | Shortcuts/CommandPalette/Breadcrumb/Audit-UI-Referenz | general ses_eb93435b3ffe7PMqEKHwW6lIby | ✅ fertig | ✅ SO (Undo/Redo, Ctrl+Shift+F-Bedingung) | Build ✅ |
| T-PEPP-A Komponenten-Projekt | projekt/*: Asides, Steps, Tabs, Cards, FileTree, del/ins, Code-Titel | general ses_eb9201da2ffe9RwCS1tpEdQoIu | ✅ fertig (alle Dateien ≤+17) | — (nur Darstellung) | Build ✅ |
| T-PEPP-B Komponenten-Rest | admin+reference+server+einstieg+index: gleiche Kur | general ses_eb9201d99ffe98zEJW1GP81efn | ✅ fertig (Code-Titel überall, Cards, del/ins) | — (nur Darstellung) | Build ✅ |
| T-KIANW KI-Anwender-Guide | NEU anwender/ki-nutzen.md + Mock-SSE-Screenshots (corporate) | general ses_eb9201d98ffeg0qr36c5jKrRKD | ✅ fertig (3 Shots, Datenverbleib ergänzt) | ✅ SO (Banner-Fußnote, Sprachausgabe-Fix) | Build ✅ |
| P-BLIND Blind-Read | Mini-Hook nur aus Doku bauen | general ses_eb93b2073ffemiWkRrQhctSfXR | ✅ DURCHGEFALLEN = gearbeitet (7 FAILs → alle am Code verifiziert + in hooks-schreiben/fehlercodes behoben) | — (Test-Ergebnis selbst) | 0 offene FAILs |
Kontrollen: Depth-Gate im Skript (gemapptes Item < Stufe Signatur = Fail), Blind-Read-Test pro Los (Generalist baut Mini-Hook nur aus der Seite), SO-Spotcheck 3 Zufalls-Funktionen gegen Code.
## Orga-Checkliste (Runde 3)
- [x] Plan-Datei angelegt (`plans/doku-ausbau.md`)
- [x] Welle 1 gestartet (T-REF, T-PROJ-A, T-SERVER) — 3 Agenten laufen im Hintergrund
- [x] Welle 1 Ergebnisse eingetroffen (je Paket: Dateien geschrieben?)
- [x] Welle 1 Build + Links geprüft
- [x] Second Opinion T-REF, T-PROJ-A, T-SERVER eingeholt und eingearbeitet
- [x] Welle 2 Ergebnisse + Build + Second Opinions (alle drei Pakete fertig)
- [x] Welle 3 Ergebnisse + Build + Second Opinions (alle drei Pakete fertig)
- [x] T-SHOTS: Screenshots erstellt und eingebunden (24 webp aus corporate, Text-Abweichungen korrigiert)
- [x] T-FINISH: Index, Stempel, Linkcheck, AGENTS-Spiegelung (55 Seiten, 0 Hüllen, ADR-0003)
- [x] Abschlussmeldung an User
- [x] Stil-Guide professionell-per-du in AGENTS.md (§1)
- [x] T-PB Ergebnis + Build + Second Opinion
- [x] T-COV Inventar + Map, Lückenzahl steht (282/282, 0 OFFEN — Map v1 Topic-Level bei ui/*)
- [x] T-TON gestartet (alle 10 Anwender-Seiten)
- [x] T-TON Ergebnis + Build + Second Opinion
- [x] T-SHOTS-2 gestartet (Overlay + Viewport, corporate)
- [x] T-SHOTS-2 Ergebnis eingebaut
- [x] Abschlussmeldung Runde 2 an User
## Orga-Checkliste Runde 3 (Tiefenausbau)
- [x] P-DEPTH gestartet (Messlatte, Gate)
- [x] P-DEPTH Ergebnis: Funktionskörnung + Depth-Mode, verifiziert (66 Symbole, ehrliche Null auf 3/4)
- [x] P-HOOKS-1 gestartet (Muster-Los Daten)
- [x] P-HOOKS-1 Ergebnis + Blind-Read + SO (27 Symbole Stufe 4, Scanner per-###, Zitat-Regel)
- [x] P-HOOKS-2 bis 5 parallel gestartet (nur Kapitel-Dateien, Shared-Edits beim Orga)
- [ ] Lose 2-5 eingetroffen: Sidebar + Index-Links + Map-Umtrag (Orga), Build, SO-Spotcheck
- [ ] Blind-Read-Test über alle 5 Kapitel (Mini-Hook nur aus Doku)
- [x] P-MCP-D + P-LLM-D + P-UI-D starten
- [x] Abschlussmeldung Runde 3 an User
- [ ] Abschlussmeldung Runde 3 an User
- [ ] Offen an tibi-server: Typo `reponse.header` im Server-Code (`context_response.go`, von Doku wörtlich zitiert) → Issue
- [ ] Offen an tibi-admin-nova: totes `u`-Kürzel (`toggleUsers`-Key fehlt in `de.json`, nur Akkord `g → u` funktioniert) → Issue
- [ ] Abschlussmeldung Runde 3 an User
+39
View File
@@ -0,0 +1,39 @@
# T-SHOTS Arbeitsauftrag: Screenshots für die Anwender-Doku
Verbindlich für den Agenten, der T-SHOTS ausführt. Stand: 2026-10-16.
## Eiserne Regeln
- **Mock-Projekt: `corporate` (Corporate Website).** NIEMALS `t1` (Test Core — Playwright-Minimalprojekt mit Quatsch-Daten) oder andere Test-Namespaces.
- Anmeldung im Mock-Admin: `admin` / `admin` (siehe `frontend/assets/mock/login.json`).
- Admin im Mock-Modus starten (siehe `tibi-admin-nova/README.md`, Docker-Stack mit Mock-Profil). Kein echter Server nötig.
- Bilder nach `tibi-docs/src/assets/anwender/`, max 200 KB pro Datei (komprimieren, z. B. `cwebp`/`pngquant`), deutsche Alt-Texte direkt im Markdown (`![Alt-Text](...)`).
- 1 Bild pro 3–4 Klicks, exakt an der Stelle des HTML-Kommentars `<!-- Screenshot (T-SHOTS): ... -->` in der jeweiligen Seite einfügen (Kommentar dabei entfernen).
- Danach `npm run build` in tibi-docs — muss grün bleiben.
## Warum corporate
`frontend/assets/mock/project.json`: `corporate` → „Corporate Website — Hauptwebsite der Firma mit Blog, Team-Seite und Kontaktformular". Sammlungen (`_config/corporate/collections/`): `pages` (mit Pagebuilder!), `blog`, `team`, `navigation`, `medialib`, `downloads`, `redirects`, `settings`. Eigene Block-Registry (`corporate-blocks/registry.js` + `blocks.css`) — echte Vorschaubilder statt Platzhalter.
## Shot-Liste (je Anwender-Seite)
| Seite | Motive (in dieser Reihenfolge aufnehmen) |
|---|---|
| anmelden | Login-Formular (leer) → Fehler nach falschem Passwort → eingeloggter Start (Corporate-Projekt sichtbar) |
| orientierung | Start mit Projekt-Übersicht → geöffnetes Corporate-Projekt mit Menü → Schnellsuche (`/`) mit Ergebnis |
| lesen-suchen-filtern | Corporate-Liste (Tabelle) → Suche mit Treffer → Filter-Popover offen → Karten-Ansicht |
| eintrag-bearbeiten | Neu-Formular (Corporate-Blog) → ausgefülltes Formular → Speicher-Bestätigung → Pflichtfeld-Fehler (Stern) |
| medien | Medien-Raster (Corporate-Bilder) → „Dateien hinzufügen"-Dialog → Upload-Fortschritt → Bildeditor |
| seiten-bauen | Pagebuilder mit Corporate-Blöcken → Block-Auswahl → Vorschau (RowPreview) → veröffentlichte Seite |
| benutzer-teams | „Mein Profil" mit Rolle → Benutzerverwaltung (falls Rolle es zeigt) → Team-Ansicht |
| exportieren | Export-Dialog mit Formatwahl → Spaltenauswahl → fertige Datei |
| sichern | Snapshot-Ansicht (Reiter Snapshots) → Details → Statistik |
| hilfe | Keine Pflicht-Screenshots — nur wo sinnvoll (z. B. echte Fehlermeldung „Keine Berechtigung für diese Aktion") |
## Ablauf
1. Mock-Admin starten, als `admin`/`admin` anmelden, Projekt **Corporate Website** wählen.
2. Motive der Reihe nach aufnehmen (Browser-Screenshots, Desktop-Breite 1440, gleiche Fenstergröße für alle).
3. Zuschneiden auf Inhalt (keine Browser-Chrome-Ränder außer wo nötig), komprimieren (<200 KB).
4. In die Seiten einsetzen (Kommentar ersetzen), Alt-Texte deutsch („Anmeldemaske von tibi-admin mit ...").
5. `npm run build` prüfen. Abnahme: Testleser erkennt jeden Schritt am Bild wieder.
+387
View File
@@ -0,0 +1,387 @@
#!/usr/bin/env node
/**
* Coverage-Inventar für die Tibi-Doku.
*
* Liest CODE (tibi-server, tibi-types, tibi-admin-nova) und gleicht die
* gefundenen Items mit plans/coverage-map.json ab. Reine Lückenliste:
* Exit-Code ist immer 0, kein Fail.
*
* - stdout: kompakte Übersicht pro Kategorie (total, gemappt, OFFEN, max 40)
* plus je Kategorie eine Depth-Zeile (Stufe 1/2/3/4)
* - plans/coverage-report.json: Zusammenfassung als JSON (inkl. depth)
*
* Körnung: contexts und tools sind Symbole (z. B. "db.find"), alle anderen
* Kategorien behalten ihre bisherige Körnung. Depth-Stufen je Symbol auf
* seiner Map-Seite (src/content/docs/<slug>.md):
* 1 = Symbol fehlt (oder Seite fehlt), 2 = nur Erwähnung,
* 3 = Erwähnung + Fenced-Codeblock mit Symbol ≤30 Zeilen danach,
* 4 = zusätzlich Error-Token im selben H2-Abschnitt.
* Andere Kategorien: reiner Map-Abgleich (gemappt = Stufe 2).
*/
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const docsRoot = path.resolve(scriptDir, "..");
const cmsRoot = path.dirname(docsRoot);
const SERVER_ROOT = path.join(cmsRoot, "tibi-server");
const TYPES_ROOT = path.join(cmsRoot, "tibi-types");
const ADMIN_ROOT = path.join(cmsRoot, "tibi-admin-nova");
const MAP_PATH = path.join(docsRoot, "plans", "coverage-map.json");
const REPORT_PATH = path.join(docsRoot, "plans", "coverage-report.json");
const MAX_OPEN_STDOUT = 40;
function readText(p) {
return readFileSync(p, "utf8");
}
function warn(msg) {
console.error(`WARN: ${msg}`);
}
/** 1. Routen: apiGroup.GET/POST/PUT/DELETE("pfad") + adm.* + OPTIONS aus setup.go, Duplikate raus. */
function collectRoutes() {
const file = path.join(SERVER_ROOT, "internal", "controller", "setup.go");
if (!existsSync(file)) {
warn(`setup.go nicht gefunden: ${file}`);
return [];
}
const src = readText(file);
const found = new Set();
for (const re of [
/apiGroup\.(GET|POST|PUT|DELETE)\(\s*"([^"]+)"/g,
/\badm\.(GET|POST|PUT|DELETE)\(\s*"([^"]+)"/g,
/\.OPTIONS\(\s*"([^"]+)"/g,
]) {
let m;
while ((m = re.exec(src)) !== null) found.add(m[m.length - 1]);
}
return [...found].sort();
}
/**
* 2. Hook-Symbole in Funktionskörnung: je context_*.go (ohne _test) die
* JS-Aufrufnamen aus RegisterPackageFunctions("paket", ...{"funktion": GoIdent})
* plus Register("name", GoIdent) (Sonderfall request). Item-Format
* "paket.funktion" bzw. "request". Dazu je Symbol: Go-Datei, Go-Ident und
* ob ein `args:`-Kommentar im direkt darüberstehenden Kommentarblock steht.
*/
function collectContextSymbols() {
const dir = path.join(SERVER_ROOT, "internal", "hook");
if (!existsSync(dir)) {
warn(`hook-Verzeichnis nicht gefunden: ${dir}`);
return [];
}
const out = [];
const files = readdirSync(dir)
.filter((f) => f.startsWith("context_") && f.endsWith(".go") && !f.endsWith("_test.go"))
.sort();
for (const f of files) {
const src = readText(path.join(dir, f));
const lines = src.split("\n");
const pkgRe = /RegisterPackageFunctions\(\s*"([^"]+)"\s*,\s*models\.ContextPackageFunctions\{(.*?)\}\)/gs;
let m;
while ((m = pkgRe.exec(src)) !== null) {
const pkg = m[1];
const entryRe = /"([^"]+)"\s*:\s*([A-Za-z_][A-Za-z0-9_]*)/g;
let e;
while ((e = entryRe.exec(m[2])) !== null) {
out.push({ symbol: `${pkg}.${e[1]}`, goFile: f, goIdent: e[2], hasArgs: hasArgsComment(lines, e[2]) });
}
}
// Sonderfall Einzel-Registrierung, z. B. Register("request", FuncRequest)
const singleRe = /\bRegister\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/g;
while ((m = singleRe.exec(src)) !== null) {
out.push({ symbol: m[1], goFile: f, goIdent: m[2], hasArgs: hasArgsComment(lines, m[2]) });
}
}
out.sort((a, b) => (a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0));
return out;
}
/** Enthält der zusammenhängende `//`-Block direkt über `func <ident>(` ein `args:`? */
function hasArgsComment(lines, goIdent) {
const idx = lines.findIndex((l) => new RegExp(`^func\\s+${goIdent}\\s*\\(`).test(l));
if (idx < 0) return false;
const block = [];
let j = idx - 1;
while (j >= 0 && /^\s*\/\//.test(lines[j])) {
block.unshift(lines[j]);
j--;
}
return block.some((l) => /(args:|JS signature:)/i.test(l));
}
/** 3. MCP-Tools: Name: "..." aus internal/mcp/tools_*.go. */
function collectTools() {
const dir = path.join(SERVER_ROOT, "internal", "mcp");
if (!existsSync(dir)) {
warn(`mcp-Verzeichnis nicht gefunden: ${dir}`);
return [];
}
const files = readdirSync(dir).filter((f) => f.startsWith("tools_") && f.endsWith(".go"));
const found = new Set();
const re = /Name:\s*"([^"]+)"/g;
for (const f of files) {
const src = readText(path.join(dir, f));
let m;
while ((m = re.exec(src)) !== null) found.add(m[1]);
}
return [...found].sort();
}
/**
* 4. Schemas: Dateinamen aus schemas/config/*.schema.json plus deren
* Top-Level-properties-Keys als Detail (für den Report).
*/
function collectSchemas() {
const dir = path.join(TYPES_ROOT, "schemas", "config");
const detail = {};
if (!existsSync(dir)) {
warn(`schemas-Verzeichnis nicht gefunden: ${dir}`);
return { items: [], detail };
}
const files = readdirSync(dir)
.filter((f) => f.endsWith(".schema.json"))
.sort();
for (const f of files) {
try {
const parsed = JSON.parse(readText(path.join(dir, f)));
const props = parsed && typeof parsed === "object" && parsed.properties && typeof parsed.properties === "object"
? Object.keys(parsed.properties)
: [];
detail[f] = props.sort();
} catch {
warn(`Schema nicht parsbar: ${f}`);
detail[f] = [];
}
}
return { items: files, detail };
}
/** 5. Admin-Komponenten: *.svelte unter frontend/src, relativ, ohne Endung. */
function collectComponents() {
const srcDir = path.join(ADMIN_ROOT, "frontend", "src");
if (!existsSync(srcDir)) {
warn(`frontend/src nicht gefunden: ${srcDir}`);
return [];
}
const out = [];
const walk = (dir) => {
for (const entry of readdirSync(dir)) {
const full = path.join(dir, entry);
if (statSync(full).isDirectory()) {
walk(full);
} else if (entry.endsWith(".svelte")) {
const rel = path.relative(srcDir, full).split(path.sep).join("/");
out.push(rel.slice(0, -".svelte".length));
}
}
};
walk(srcDir);
return out.sort();
}
/** 6. Test-Pläne: Dateinamen aus docs/test-plans/. */
function collectTestplans() {
const dir = path.join(ADMIN_ROOT, "docs", "test-plans");
if (!existsSync(dir)) {
warn(`test-plans nicht gefunden: ${dir}`);
return [];
}
return readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
}
const ERROR_TOKENS = ["Error", "error", "400", "401", "403", "404", "409", "429", "500", "nil", "verweigert", "denied"];
/** Kategorien mit Depth-Mode (Symbol-Tiefe auf der Map-Seite). */
const DEPTH_CATS = ["contexts", "tools"];
const docCache = new Map();
function loadDocLines(slug) {
if (docCache.has(slug)) return docCache.get(slug);
const p = path.join(docsRoot, "src", "content", "docs", `${slug}.md`);
const lines = existsSync(p) ? readText(p).split("\n") : null;
docCache.set(slug, lines);
return lines;
}
/** Fenced-Codeblock-Spannen als [startFence, endFence]-Paare (0-basiert). */
function fencedSpans(lines) {
const spans = [];
let start = -1;
for (let i = 0; i < lines.length; i++) {
if (/^\s*```/.test(lines[i])) {
if (start < 0) start = i;
else {
spans.push([start, i]);
start = -1;
}
}
}
return spans;
}
/**
* Depth-Stufe je Symbol auf seiner Map-Seite:
* 1 = fehlt (kein Map-Eintrag, Seite fehlt oder Symbol nicht als Text),
* 2 = nur Erwähnung, 3 = + Fenced-Codeblock mit Symbol ≤30 Zeilen danach,
* 4 = + Error-Token im selben H2-Abschnitt (bis zur nächsten H2).
*/
function symbolDepth(symbol, slug) {
if (!slug) return 1;
const lines = loadDocLines(slug);
if (!lines) return 1;
// Aliase: "paket.fn" gilt auch als "context.paket.fn" bzw. "context.fn" (Hook-Schreibweise)
const aliases = [symbol, `context.${symbol}`];
const dot = symbol.indexOf(".");
if (dot > 0) aliases.push(`context.${symbol.slice(dot + 1)}`);
const mentions = [];
lines.forEach((l, i) => {
if (aliases.some((a) => l.includes(a))) mentions.push(i);
});
if (mentions.length === 0) return 1;
const spans = fencedSpans(lines);
let anchor = -1;
for (const mi of mentions) {
const hit = spans.some(
([s, e]) => s > mi && s <= mi + 30 && lines.slice(s, e + 1).some((l) => aliases.some((a) => l.includes(a))),
);
if (hit) {
anchor = mi;
break;
}
}
if (anchor < 0) return 2;
// Abschnitt = eigene ###-Sektion (bis zum nächsten ### oder ##), nicht das ganze Paket:
// sonst zählt der Fehler-Token fremder Symbole (Mess-Loophole).
let start = 0;
for (let k = anchor; k >= 0; k--) {
if (/^####?\s/.test(lines[k])) {
start = k;
break;
}
}
let stop = lines.length;
for (let k = anchor + 1; k < lines.length; k++) {
if (/^####?\s/.test(lines[k])) {
stop = k;
break;
}
}
const section = lines.slice(start, stop).join("\n");
// Explizit ausgewiesene Abwesenheit ("kein Fehlerfall") zählt als dokumentiert.
if (/kein Fehlerfall/i.test(section)) return 4;
return ERROR_TOKENS.some((t) => section.includes(t)) ? 4 : 3;
}
function loadMap() {
const empty = { routes: {}, contexts: {}, tools: {}, schemas: {}, components: {}, testplans: {} };
if (!existsSync(MAP_PATH)) {
warn(`keine coverage-map gefunden (${path.relative(docsRoot, MAP_PATH)}), alles OFFEN`);
return empty;
}
try {
const parsed = JSON.parse(readText(MAP_PATH));
for (const key of Object.keys(empty)) {
if (parsed[key] && typeof parsed[key] === "object") empty[key] = parsed[key];
}
return empty;
} catch {
warn("coverage-map.json nicht parsbar, alles OFFEN");
return empty;
}
}
function main() {
const schemas = collectSchemas();
const ctxSymbols = collectContextSymbols();
const ctxBySymbol = new Map(ctxSymbols.map((s) => [s.symbol, s]));
const inventory = {
routes: collectRoutes(),
contexts: ctxSymbols.map((s) => s.symbol),
tools: collectTools(),
schemas: schemas.items,
components: collectComponents(),
testplans: collectTestplans(),
};
const map = loadMap();
const report = { generatedAt: new Date().toISOString(), categories: {}, depth: {} };
let totalAll = 0;
let openAll = 0;
for (const [cat, items] of Object.entries(inventory)) {
const mapping = map[cat] ?? {};
// "__ignore": exakte Items oder Präfixe mit "*" (Infra ohne Doku-Fläche, z. B. Mock-Daten, Tests)
const ignores = Array.isArray(mapping.__ignore) ? mapping.__ignore : [];
const isIgnored = (item) =>
ignores.some((pat) => (pat.endsWith("*") ? item.startsWith(pat.slice(0, -1)) : item === pat));
const open = items.filter((item) => !(item in mapping) && !isIgnored(item));
totalAll += items.length;
openAll += open.length;
report.categories[cat] = {
total: items.length,
mapped: items.length - open.length,
openCount: open.length,
openItems: open,
};
console.log(`${cat}: total=${items.length} gemappt=${items.length - open.length} OFFEN=${open.length}`);
for (const item of open.slice(0, MAX_OPEN_STDOUT)) console.log(` OFFEN: ${item}`);
if (open.length > MAX_OPEN_STDOUT) {
console.log(` ... +${open.length - MAX_OPEN_STDOUT} weitere (siehe plans/coverage-report.json)`);
}
// Depth-Mode: contexts/tools je Symbol auf der Map-Seite, Rest Map-Abgleich.
const depth = { total: items.length, stufe4: 0, stufe3: 0, stufe2: 0, stufe1: 0 };
if (DEPTH_CATS.includes(cat)) {
const details = [];
for (const item of items) {
if (isIgnored(item)) {
depth.stufe2 += 1;
details.push({ symbol: item, page: null, stufe: 2, ignored: true });
continue;
}
const page = mapping[item] ?? null;
const stufe = symbolDepth(item, page);
depth[`stufe${stufe}`] += 1;
const detail = { symbol: item, page, stufe };
if (cat === "contexts") {
const sym = ctxBySymbol.get(item);
detail.goFile = sym?.goFile ?? null;
detail.hasArgs = sym?.hasArgs ?? false;
}
details.push(detail);
}
details.sort((a, b) => a.stufe - b.stufe || (a.symbol < b.symbol ? -1 : 1));
report.categories[cat].symbols = details;
} else {
depth.stufe2 = items.length - open.length;
depth.stufe1 = open.length;
}
report.depth[cat] = depth;
console.log(
`depth ${cat}: total=${depth.total} stufe4=${depth.stufe4} stufe3=${depth.stufe3} stufe2=${depth.stufe2} stufe1=${depth.stufe1}`,
);
if (cat === "contexts") {
const noArgs = ctxSymbols.filter((s) => !s.hasArgs).map((s) => s.symbol);
console.log(` ohne args-Kommentar (${noArgs.length}): ${noArgs.join(", ") || "–"}`);
}
}
console.log(`GESAMT: total=${totalAll} OFFEN=${openAll}`);
report.schemasDetail = schemas.detail;
writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2) + "\n", "utf8");
console.log(`Report: ${path.relative(process.cwd(), REPORT_PATH)}`);
}
try {
main();
} catch (err) {
console.error(`FEHLER: ${err?.message ?? err} (reine Liste, kein Fail)`);
}
process.exit(0);
+2 -2
View File
@@ -3,7 +3,7 @@
* sync-server-docs.mjs — Single Source bleibt in den Quell-Repos,
* diese Doku rendert nur. Kopiert:
* - ../tibi-server/docs/*.md -> src/content/docs/server/ (ohne uebersicht.md zu überschreiben)
* - ../tibi-admin-nova/docs/*.md -> src/content/docs/nova/ (+ test-plans/)
* - ../tibi-admin-nova/docs/*.md -> src/content/docs/admin/ (+ test-plans/)
* Fügt fehlendes Starlight-Frontmatter (title) aus Dateinamen/H1 hinzu.
*/
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
@@ -14,7 +14,7 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const serverSrc = join(root, '..', 'tibi-server', 'docs');
const novaSrc = join(root, '..', 'tibi-admin-nova', 'docs');
const serverDst = join(root, 'src', 'content', 'docs', 'server');
const novaDst = join(root, 'src', 'content', 'docs', 'nova');
const novaDst = join(root, 'src', 'content', 'docs', 'admin');
const titleFromName = (f) =>
f.replace(/\.md$/, '').replace(/^\d+-/, '').split('-')
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+91
View File
@@ -0,0 +1,91 @@
---
title: 'Aktionen'
description: 'Actions im Admin starten: Parameter ausfüllen, ausführen, Ausgabe und Fortschritt lesen.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du stößt im **tibi-admin** (Verwaltungs-Backend, Release Nova) eine **Action** an — z. B. Versand, Import oder Aufräum-Job. Du füllst Parameter aus, klickst Ausführen und liest das Ergebnis. Diese Seite beschreibt die Bedienung — das Schreiben von Actions steht unter [Jobs und Actions](/projekt/jobs-actions/).
## Starten
<Steps>
1. Projekt öffnen, Sammlung **Actions** (Seitenleiste) wählen.
2. Action anklicken — Route `/projects/:projekt/actions/:name`.
3. Methode per Reiter wählen (`GET` oder `POST`; die Wahl steht auch als `?method=` in der URL).
4. Formular ausfüllen, **Ausführen** klicken.
5. Ergebnis unterhalb lesen (Karte „Letztes Ergebnis“).
</Steps>
| Element | Verhalten |
|---|---|
| Methoden-Reiter | Nur `GET`/`POST` sind ausführbar (`runnableMethods` in `ActionView.svelte`); `PUT`/`DELETE` erscheinen höchstens als Ansicht, nicht als Startknopf |
| Ausführen-Knopf | Aktiv nur bei Startrecht **und** gültigem Formular; während der Ausführung dreht sich das Icon und der Knopf ist gesperrt |
| Upload-Toast | Enthält die Eingabe Datei-Bytes, zeigt der Admin einen Byte-Fortschritt (`upload-progress`) |
| Verlauf | Werkzeug-Menü (Schild-Icon) → Audit mit Filter `collection=_actions/<name>` |
<!-- Screenshot-Platzhalter (T-SHOTS): Action-Ansicht mit Methoden-Reitern, Eingabeformular und Ausführen-Knopf. -->
## Parameter
Das Formular (`ActionMethodScaffold.svelte`) trennt zwei Blöcke:
<Badge text="query" variant="note" /> <Badge text="data" variant="tip" />
| Block | Wohin gesendet |
|---|---|
| Abfrage-Felder (`query`) | Query-String der Action-URL (`?feld=wert`; Arrays wiederholt) |
| Daten-Felder (`data`) | JSON-Body (`POST`/`PUT`); bei `GET` kein Body |
- Felder kommen aus `action.meta.methods.<methode>.query.fields` bzw. `data.fields` und nutzen dieselben [Widgets](/admin/eintrags-editor/) wie der Editor (inklusive `dependsOn`-Sichtbarkeit).
- Validierung läuft live: Fehler erscheinen nach Verlassen des Felds (`blur`), der Knopf bleibt bis zur Korrektur gesperrt und nennt die Fehlerzahl.
- **Technische Vorschau** (aufklappbar unter dem Formular) zeigt das exakte Payload-JSON `{ query, data }`.
- Server-Aufruf: `POST /api/v1/<namespace>/_actions/<name>?…` mit Sitzungs-Kopf `X-Auth-Token` (siehe [Token-Köpfe](/reference/token-koepfe/)).
## Ausgabe und Fortschritt
<Tabs>
<TabItem label="Kurfassung und Details">
`ActionOutputMapping.svelte` zeigt gemappte Felder statt Roh-JSON:
- **Kurzfassung** (`output.mapping.summary.fields`) — immer sichtbar, Werte per `source`-Pfad aus dem Ergebnis gelesen (`deepGet`, z. B. `totals.grandTotal`).
- **Details** (`output.mapping.details.fields`) — aufklappbar, als Widgets gerendert.
- Ohne Mapping zeigt der Admin das Ergebnis als formatiertes JSON; kennt die Methode nur einen Titel ohne Felder, steht dort „keine Ausgabefelder“.
</TabItem>
<TabItem label="Fortschritt (SSE)">
Liefert die Action Live-Events (`Content-Type: text/event-stream`), zeigt `ActionProgressMapping.svelte`:
- Balken aus `index` / `count` (`index/count × 100`, sonst Prozent oder Zählertext).
- Status- und Meldungstext aus `progress.statusField` / `progress.messageField`.
- Optionale Zusatzfelder (`progress.fields`) aus jedem Event.
<Aside type="note">Ohne `output.mapping.progress` in der Action-Meta gibt es bewusst **keinen** Balken — ein 0-%-Balken bei jeder alten Action wäre irreführend. Dann bleibt JSON das Ergebnis.</Aside>
</TabItem>
</Tabs>
```text title="Action-Ablauf im Überblick"
Formular (query + data) → Ausführen → [SSE-Events → Balken] → Ergebnis (Mapping oder JSON)
└─ Verlauf: Audit mit _actions/<name>
```
## Rechte und Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Kein Startknopf / „gesperrt“ | Methode nicht in deinen Rechten (`project.yourPermissions._actions.<name>`: `get`/`post`-Flag fehlt) | Rechte unter [Rechte und Tokens](/projekt/rechte-tokens/) klären |
| Formularfehler genannt | Pflicht-/Formatfehler im Eingabeblock | Felder korrigieren — die Vorschau zeigt, was gesendet würde |
| Servertext im roten Kasten | Hook oder Server meldet fachlichen Fehler | Meldung wörtlich nehmen (der Admin reicht sie durch) |
| `permission` / `not found` | Falscher Kopf oder Action gelöscht | [Token-Köpfe](/reference/token-koepfe/) und Action-Namen prüfen |
| Netzwerkfehler | Server nicht erreichbar | [Admin einrichten](/admin/setup/) prüfen, erneut versuchen |
<Aside type="caution">Actions können Daten ändern. Starte eine unbekannte Action erst in einer Testumgebung — der Verlauf (`_actions/<name>` im Audit) zeigt nur, was bereits lief.</Aside>
Klick-Abläufe dazu stehen im Test-Plan `docs/test-plans/40-actions.md` (Methoden-Tabs, ausführbar vs. gesperrt, Ergebnis und Fortschritt).
Verifiziert am 2b091d899590b392491c59a54f50de43f1057280 2026-10-16 (tibi-admin-nova)
+105
View File
@@ -0,0 +1,105 @@
---
title: 'Ansichten und Listen'
description: 'Tabellen-, Karten-, Medien- und Navigations-Ansichten, Suche, Sortierung, Filter, Blättern.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge } from '@astrojs/starlight/components';
Du öffnest eine Sammlung im **tibi-admin** (Verwaltungs-Backend, Release Nova) und siehst eine Liste aller Einträge. Suche, Sortierung, Filter und Blättern funktionieren in allen Ansichten gleich. Diese Seite beschreibt die Bedienung aus Redakteurs- und Projekt-Sicht — Konzept und Einordnung stehen unter [Admin-Konzept](/admin/konzept/).
## Ansichtsarten
Die Sammlung schlägt ihre Startansicht über `meta.viewHint` vor (Details: [Sammlungen anlegen](/projekt/sammlungen-anlegen/)). Du wechselst die Ansicht jederzeit per Ansichtswähler — deine Wahl landet im URL-Parameter `view`.
<Badge text="table" variant="note" /> <Badge text="cards" variant="tip" /> <Badge text="media" variant="note" /> <Badge text="navigation" variant="note" />
| Ansicht | `viewHint` | Wofür |
|---|---|---|
| Tabelle | — (Standard) | Viele Einträge vergleichen, Spalten sortieren, Mehrfachauswahl |
| Karten | `cards` | Vorschau mit Bild, Titel und Zusatzzeilen (`meta.preview`: `label`, `secondary`, `tertiary`, `image`) |
| Medien | `viewHint.media` | Raster mit Drag-and-drop-Upload direkt über dem Raster (siehe [Medien](/admin/medien/)) |
| Navigation | `viewHint.navigation` | Baumvorschau je Navigationsbaum (`nodesField`, `declaredTrees`) |
<Aside type="note">
`viewHint` ist nur der Startwert. Sobald `view=…` in der URL steht, gilt deine Wahl — auch nach Neuladen.
</Aside>
Die Tabelle zeigt die Spalten aus `preview.table` oder erkennt sie automatisch aus den Feldern. Ein Klick auf einen Spaltenkopf sortiert auf- oder absteigend. Systemfelder der Eintrags-Übersetzung (`translationKey`, `lang`) blendet die Tabelle in der Kopfzeile aus — sie bleiben über die [Übersetzungsleiste](/admin/eintrags-editor/) erreichbar.
### Unter-Navigation
Manche Sammlungen haben Reiter über der Liste (`meta.subNavigation`, z. B. `?sub=published`). Jeder Reiter hat eigenen Filter, eigene Standardsortierung und eigene Trefferzahl (parallele `/_count`-Aufrufe). Die aktive Auswahl steht im URL-Parameter `sub`.
## Suchen, Sortieren, Filtern
<Tabs>
<TabItem label="Suche">
- Suchfeld oben über der Liste, entprellt während der Eingabe.
- Bei laufender Suche gilt **Relevanz-Sortierung**: eine manuell gewählte Sortierung wird verworfen, der Server rankt nach Trefferqualität. Du kannst danach erneut sortieren.
- Hat die Sammlung mehrere Suchmodi (`collection.search[].meta`), wählst du den Modus am Suchfeld. Wechsel-Tastenkürzel: `Strg + Umschalt + F`.
- Die Trefferzahl für Filter-Warnungen (z. B. vor [Exporten](/admin/exporte/)) kommt aus der `/_count`-Route, nicht aus der geladenen Seite.
</TabItem>
<TabItem label="Sortierung">
- Spaltenkopf anklicken oder Sortier-Popover öffnen (`o`-Taste).
- Standard ohne Auswahl: Feld `updateTime`, absteigend — oder `meta.defaultSort` der Sammlung bzw. des aktiven Reiters.
- Bei Suche ohne manuelle Sortierung entfällt `sort` in Anfrage und URL (Relevanz).
</TabItem>
<TabItem label="Filter">
- Filter-Popover öffnen (`f`-Taste). Zurücksetzen: `x`-Taste.
- Filterbare Felder meldet das Schema per `meta.filter` an — inklusive Unterfelder von Objekten, die sich per `meta.filter` anmelden.
- Ausgeschlossen sind standardmäßig: Fremd-Medien-Widgets (`foreignMedia`, `foreignFile` u. ä.), native Datei-Felder und reine Objekt-Kästen ohne angemeldete Unterfelder.
- Fremdschlüssel-Felder bieten eine Auswahl mit Suche statt Freitext.
- Die Vorschläge für Auswahlwerte (`_facet`-Route) liefern je Wert die Trefferzahl mit.
</TabItem>
<TabItem label="Blättern">
- Fußzeile mit Seitenzahlen, Ellipsen bei vielen Seiten und Anzeige „Eintrag X–Y von Z“.
- Seitengröße umschaltbar (Standard 25). Tastatur: `[` / `]` für vor/zurück, `p` + Ziffer für direkte Seite, `p` + `0` wechselt die Seitengröße.
- Die Gesamtzahl lädt der Admin über `GET …/<sammlung>/_count` — Details zu den Routen stehen unter [Lesen und suchen](/projekt/abfragen-lesen/).
</TabItem>
</Tabs>
## Mehrfachauswahl und Tastatur
- Auswahl per Checkbox, `Leertaste` oder `a` (alle umschalten). Pfeiltasten bewegen den Fokus, `Enter` öffnet den Eintrag.
- Mit Auswahl öffnest du die Massenbearbeitung (`e`-Taste: Werte setzen, Bedingungen prüfen, Vorschau) oder das Löschen mit Referenzen-Prüfung.
- Neue Einträge: `n`-Taste. Befehlspalette und Feldsuche im Editor stehen unter [Eintrags-Editor](/admin/eintrags-editor/).
```text title="URL-State einer Liste"
# URL-State einer Liste — kopierbar und teilbar
/content?view=cards&q=hero&qm=combined&sort=name&order=asc&page=2&size=25&sub=published&f_lang=de
```
| Parameter | Bedeutung | Standard |
|---|---|---|
| `q` | Suchtext | leer (keine Suche) |
| `qm` | Suchmodus (`qName` aus dem `search`-Block der Sammlung) | Standard-Modus |
| `sort` / `order` | Sortierfeld und -richtung | `updateTime` / `desc` (oder `meta.defaultSort`) |
| `page` / `size` | Seite und Seitengröße | `1` / `25` |
| `view` | `table`, `cards`, `media`, `navigation` | `table` |
| `sub` | Aktiver Unter-Reiter | keiner |
| `f_*` | Feld-Filter, z. B. `f_lang=de` (Objekte als JSON) | keine |
<Aside type="tip">
Teile eine gefilterte Liste per URL — Empfänger sehen exakt dieselbe Auswahl, weil der Zustand vollständig in der Adresse steht.
</Aside>
## Häufige Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Liste leer trotz Einträgen | Reiter-Filter (`sub`) oder Feld-Filter aktiv | `x` drücken oder Filter-Popover leeren |
| Sortierung „springt zurück“ | Suche aktiv → Relevanz schlägt manuelle Sortierung | Suchtext leeren oder Sortierung nach der Suche erneut wählen |
| Gesamtzahl fehlt oder lädt lange | `/_count` auf altem Server nicht verfügbar | Direkt laden lassen (Fallback) oder Server aktualisieren |
| Ansicht startet „falsch“ | `viewHint` der Sammlung schlägt die Startansicht vor | Ansicht einmal wechseln — `view=…` in der URL gewinnt |
<!-- Screenshot-Platzhalter (T-SHOTS): Listenansicht mit Suchfeld, Filter-Popover und Paginierung, Alt-Text deutsch, max. 200 KB unter src/assets/. -->
Verifiziert am 2b091d89 2026-10-13 (tibi-admin-nova)
+107
View File
@@ -0,0 +1,107 @@
---
title: 'Benutzer, Teams, Tokens'
description: 'Anmelden, Personen, Teams und Orgs verwalten, Rollen und Berechtigungen, PATs, Stellvertretung.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du verwaltest im **tibi-admin** (Verwaltungs-Backend, Release Nova), wer sich anmelden und was jede Person darf: Benutzerkonten, Teams, Organisationen, Rollen, persönliche Token (PATs) und die Stellvertretung. Server-Regeln dazu stehen unter [Rechte und Tokens](/projekt/rechte-tokens/), die vier HTTP-Köpfe unter [Token-Köpfe](/reference/token-koepfe/).
## Anmelden und Sitzung
<Steps>
1. Login-Seite öffnen (`LoginPage.svelte`): Benutzername und Passwort eingeben.
2. Bei Erfolg legt der Admin ein JWT in den Store und plant die Auffrischung (`scheduleTokenRefresh`); danach landet du auf `/projects`.
3. Bei Sitzungsende warnt `SessionTimeoutWarning.svelte`: **60-Sekunden-Countdown** mit Verlängern- oder Abmelden-Knopf — ohne Reaktion folgt die Abmeldung.
4. Sprache (DE/EN) und Theme stellst du auf der Login-Karte um.
</Steps>
| Verhalten | Quelle |
|---|---|
| Fehlgeschlagener Login | Rote Fehlerbox, keine Weiterleitung |
| Inaktivitäts-Timer | `utils/inactivityTracker` + Countdown-Dialog (`resetInactivityTimer`) |
| Eigene Daten ändern | Profilseite („Mein Profil"): Name, E-Mail, Firma, Abteilung, Avatar, Passwort (min. 6 Zeichen, doppelt bestätigt) |
## Personen, Teams, Orgs
Die Kette lautet **Org → Team → Benutzer → Projekt**. Du pflegst sie im Admin-Hub (`components/admin/`):
| Ebene | Komponenten | Inhalt |
|---|---|---|
| Benutzer | `UserTab` (Liste mit Suche, Filter, Sortierung, Paginierung), `UserCreateDrawer`, `UserDetailDrawer` | Name, Login, Rolle, Org-/Team-Mitgliedschaften |
| Teams | `TeamTab`, `TeamCreateDrawer`, `TeamDetailDrawer` | Name, Farbe, Org-Zugehörigkeit, Mitglieder, Berechtigungen |
| Orgs | `OrgTab`, `OrgCreateDrawer`, `OrgDetailDrawer` | Name, Mitglieder (`OrgAssignmentSection`), Projekte |
| Projekte | `ProjectAdminView`, `ProjectDetailDrawer`, `ProjectCreateDrawer` | Name, Status (online/offline), Org-Zuordnung, Rechte-Matrix |
<Steps>
1. Org anlegen (`OrgCreateDrawer`).
2. Team in dieser Org anlegen (`TeamCreateDrawer`).
3. Benutzer anlegen und Team zuordnen (`UserCreateDrawer`, Reiter Mitgliedschaften).
4. Projekt der Org zuordnen und Team-Rechte setzen (`ProjectDetailDrawer`).
</Steps>
## Rollen und Chips
<Badge text="role: 0" variant="tip" /> <Badge text="role: 1" variant="note" />
| Rolle | Wert | Bedeutung |
|---|---|---|
| Admin | `role: 0` | Vollzugriff, violettes Badge (`RoleSection.svelte`) |
| Benutzer | `role: 1` | Zugriff nur über Org-/Team-Mitgliedschaft, neutrales Badge |
- Die Rolle stellst du per Auswahl um (mit Admin-Warnung); im Nur-Lesen-Modus zeigt der Admin nur das Badge.
- Feinrechte vergibst du als **Chips** (`PermissionChipsInput.svelte`): Eintippen, Vorschlag anklicken oder Entfernen per ×. Vorschläge kommen aus der Projekt-Meta (`project-meta`) oder als Team-Rückfall (`team-fallback`); Platzhalter-Beispiel: `pages, media`.
## Tokens
Persönliche Token (PATs) pflegst du auf zwei Wegen: im eigenen **Profil** (`TokenSettings.svelte`) oder als Admin im **Token-Reiter** (`TokenAdminTab.svelte`).
<Tabs>
<TabItem label="Erstellen">
1. **Neu** klicken → `TokenFormDrawer`: Name plus Scopes per `TokenScopePicker` wählen.
2. Speichern → `TokenCreatedDialog` zeigt den Wert **genau einmal** — kopiere ihn sofort, danach ist er weg.
3. Bearbeiten ändert nur wirklich geänderte Felder (Name/Scopes als Patch).
</TabItem>
<TabItem label="Verwerfen und Grenzen">
- Widerrufen per Löschen (`deleteToken` → Liste neu laden).
- Antwortet der Server mit `404` auf die Token-Liste, blendet der Admin den Bereich still aus (Feature-Erkennung `isTokenFeatureDisabled`).
- Aufrufe laufen mit deiner Login-Sitzung (`X-Auth-Token`-JWT via `fetchWithAuth`); Token-Verwaltung selbst braucht kein separates PAT.
- **Während einer Stellvertretung ist die Token-Verwaltung gesperrt** (`disabled`, solange `impersonatedBy` gesetzt ist).
</TabItem>
</Tabs>
## Stellvertretung (Impersonation)
Admins können sich als ein Benutzerkonto anmelden, um dessen Sicht zu prüfen:
| Schritt | Technik (`api/auth.ts`) |
|---|---|
| Starten | `POST /login/impersonate { userId }` → neues Token + `impersonatedBy`-Vermerk |
| Erkennen | Banner/Status zeigt die Stellvertretung (`AdminLayout`, `AdminHub`, `UserDetailDrawer`) |
| Beenden | `POST /login/impersonate?logout=1` → Server löscht den Stellvertretungs-Cookie und stellt die Admin-Sitzung wieder her |
<Aside type="caution">Stellvertretung nur mit Freigabe nutzen — du handelst mit den Rechten der vertretenen Person, und der Vorgang wird protokolliert (Audit). Währenddessen keine Token erstellen.</Aside>
## Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Login schlägt fehl | Falsche Zugangsdaten | Eingaben prüfen; Konto beim Admin erfragen |
| Countdown erscheint | Inaktivität | Sitzung verlängern oder neu anmelden |
| Benutzer/Team/Org fehlt | Keine Leserechte auf diese Ebene | Admin um Freigabe bitten |
| Token-Bereich leer | Server ohne Token-Feature (`404`) | Normal — kein Fehler, Bereich bleibt verborgen |
| `permission denied` | Kopf oder Recht falsch | [Token-Köpfe](/reference/token-koepfe/) und Chips prüfen |
<!-- Screenshot-Platzhalter (T-SHOTS): Hub mit Benutzerliste und Detail-Drawer inkl. Rollen-Badge und Chips. -->
Klick-Abläufe zur Stellvertretung stehen im Test-Plan `docs/test-plans/42-impersonation.md` (Auslöser, Bestätigungsdialog, Banner, Rückkehr zur Admin-Sitzung — nur im Mock-Modus testen).
Verifiziert am 2b091d899590b392491c59a54f50de43f1057280 2026-10-16 (tibi-admin-nova)
+89
View File
@@ -0,0 +1,89 @@
---
title: 'Admin anpassen'
description: 'Sammlungen, Ansichten, Widgets, Fremdschlüssel, Vorschau, Aktionen, Suche und eigene Exporte im Admin.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du passt das **tibi-admin** (Verwaltungs-Backend, Release Nova) an dein Projekt an: Was Listen zeigen, welche Eingabeelemente Formulare nutzen, wie Datensätze verknüpft und gesucht werden. Das meiste steuerst du über die Sammlungs-Config — Schema-Details stehen unter [Sammlungen anlegen](/projekt/sammlungen-anlegen/) und [Felder und Widgets](/projekt/felder-widgets/).
## Ansichten und Spalten
| Mittel | Wirkung |
|---|---|
| `meta.views` (Tabelle, Karten, Kalender …) | Welche [Ansichten](/admin/ansichten-listen/) die Sammlung anbietet |
| Sichtbare Spalten | Per Checkbox im Listen-Kopf; [Exporte](/admin/exporte/) übernehmen sie als Vorauswahl |
| URL-Parameter | Ansicht ist teilbar: `?view=cards&q=hero&qm=combined&sort=name&order=asc&page=2&size=25` — `qm` ist der Suchmodus aus dem `search`-Block |
| Sortierung | Klick auf Spaltenkopf; manuelle Reihenfolge per Drag, wo freigegeben |
## Widgets und Felder
Der `WidgetDispatcher` wählt das Eingabeelement: zuerst `meta.widget`, sonst der Feldtyp. Alle **25 Schema-Enum-Werte** kommen vor (`pagebuilder` und `pageBuilder` meinen dasselbe):
| Gruppe | Widgets |
|---|---|
| Text | `text`, `textarea`, `richtext`, `html`, `json` |
| Zahl/Auswahl | `number`, `checkbox`, `switch`, `select`, `selectArray`, `chipArray`, `color` |
| Datum | `date`, `datetime` |
| Dateien | `file`, `image`, `foreignFile`, `foreignMedia` |
| Verknüpfung | `foreignKey`, `foreignKeyChipArray` |
| Struktur | `pagebuilder`/`pageBuilder`, `templateProps`, `containerLessObject`, `containerLessObjectArray` |
- Pflicht, Formate und `dependsOn`-Sichtbarkeit kommen aus dem Feld-Schema — Bedienung siehe [Eintrags-Editor](/admin/eintrags-editor/).
- Mehrsprachige Felder bekommen automatisch Sprachtabs (`I18nLanguageTabs`).
## Fremdschlüssel und Vorschau
| Baustein | Datei | Nutzen |
|---|---|---|
| Auswahl-Dialog | `entry/ForeignKeyDrawer.svelte` | Einträge anderer Sammlung suchen und verknüpfen |
| Inline anlegen | `entry/InlineEntryForm.svelte` | Verknüpften Eintrag erzeugen, ohne die Ansicht zu verlassen |
| Rückverweise | `entry/ReferencedBy.svelte` | Zeigt, wo der Eintrag verwendet wird |
| Feldsuche | `entry/FieldSearch.svelte` | Springt zu Feldern in langen Formularen |
| Vorschau | `entry/PreviewElement.svelte` + `preview-resolver` | Löst Pfade (`deepGet`, `_lookup`) für [Medien](/admin/medien/) und gemappte [Actions](/admin/aktionen/) auf |
## Aktionen und Suche
- **Actions** pro Sammlung/projektweit hängst du als Methoden-Meta an (`query`-/`data`-Felder, Ausgabe-Mapping, Fortschritt) — ausführen wie unter [Aktionen](/admin/aktionen/) beschrieben.
- **Suche:** `search`-Block der Sammlung definiert Modi (`qm`-Parameter in der URL, z. B. `qm=combined`); Feld-Filter, Reiter-Filter (`sub`) und Freitext kombinieren sich — [Exporte](/admin/exporte/) übernehmen den Filter eins zu eins.
- **Stapel:** `BulkEditPanel` für Mehrfach-Änderungen direkt aus der Liste.
## Eigene Bausteine und Exporte
<Tabs>
<TabItem label="Custom-Export-Skript">
Projekt-Entwickler hinterlegen Berichte in `collection.meta.exports` (`name` + `file` Pflicht, z. B. `_/assets/exports/reports.mjs`). Das Modul exportiert **eine** Standard-Funktion:
```js title="Custom-Export-Skript (Beispiel)"
export default async function (ctx) {
const rows = await ctx.db.loadAll("orders", { select: ["orderNo"] });
const wb = ctx.xlsx.utils.aoa_to_sheet([["Order"], ...rows.map((r) => [r.orderNo])]);
return { workbook: { SheetNames: ["R"], Sheets: { R: wb } }, filename: "report-{date}.xlsx", contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" };
}
```
| Pflicht | Details |
|---|---|
| `ctx.db.loadAll` | Lädt in 500er-Paketen mit Fortschritt (`ctx.progress`) und Abbruch (`ctx.signal`) |
| `ctx.xlsx` | Gebündelte `xlsx-js-style`-Lib — kein Eigen-Import nötig |
| `filename` + `contentType` | Pflicht — ohne `contentType` wirft der Admin einen Fehler |
| Genau ein Payload | `workbook`, `blob`, `text` **oder** `bytes` |
Volle Kontext-Tabelle in der Repo-Doku `custom-export-script.md`; Bedienung unter [Exporte](/admin/exporte/).
</TabItem>
<TabItem label="Eigene Widgets/Seiten">
- Eigene Eingabeelemente registrierst du am `WidgetDispatcher` (Feldtyp + `meta.widget`-Name) — erst Schema in [Felder und Widgets](/projekt/felder-widgets/) prüfen.
- Neue Admin-Bereiche folgen dem Muster `components/admin/*Tab + *CreateDrawer + *DetailDrawer` mit Suche, Filter, Sortierung und Paginierung.
- KI-Kontext (`aiController`) bleibt automatisch konsistent, solange du `data-ai-id`-Attribute setzt.
</TabItem>
</Tabs>
<Aside type="caution">Anpassungen an Sammlungs-Config und Skripten erst im lokalen Stack ([einrichten](/admin/setup/)) testen — falsche `meta`-Einträge blenden Ansichten oder Export-Reiter ersatzlos aus.</Aside>
<!-- Screenshot-Platzhalter (T-SHOTS): angepasste Listenansicht mit Spaltenwahl und eigenem Export-Reiter. -->
Verifiziert am 2b091d899590b392491c59a54f50de43f1057280 2026-10-16 (tibi-admin-nova)
+105
View File
@@ -0,0 +1,105 @@
---
title: 'Eintrags-Editor'
description: 'Felder ausfüllen, Widgets, Fremdschlüssel, i18n-Tabs, Verlauf, Tastaturkürzel.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge } from '@astrojs/starlight/components';
Du öffnest einen Eintrag aus einer [Liste](/admin/ansichten-listen/) und bearbeitest seine Felder im Editor des **tibi-admin** (Verwaltungs-Backend, Release Nova). Links liegt das Formular, rechts die Seitenleiste mit Aktionen und Gruppen (`meta.position`: `main`, `sidebar:<gruppe>`, `sidebar:settings`). Diese Seite beschreibt die Bedienung — Feldtypen und Schema-Details stehen unter [Felder und Widgets](/projekt/felder-widgets/).
## Aufbau
<Steps>
1. Eintrag in der Liste anklicken oder mit `n` neu anlegen.
2. Felder im Hauptbereich ausfüllen, Gruppen in der Seitenleiste prüfen.
3. Pflichtfehler beheben — der Speichern-Knopf zeigt die ersten Fehler als Tooltip.
4. Speichern mit `Strg + S` oder Knopf. Bei Erfolg toastet der Admin die Meldung.
</Steps>
- **Ungespeicherte Änderungen:** Der Editor erkennt Abweichungen zum geladenen Stand (Sortierung der Schlüssel ignoriert er) und warnt dich.
- **Rückgängig/Wiederholen:** `Strg + Z` und `Strg + Umschalt + Z` (oder `Strg + Y`) wirken auf Feldänderungen, inklusive Array-Operationen.
- **Validierung:** Beim Verlassen eines Felds sofort, vor dem Speichern für alle sichtbaren Felder. Server-Fehler landen zusätzlich im Formular; der erste Fehler wird angescrollt.
- **Verlauf:** Über das Verlaufs-Symbol öffnest du die Audit-Historie des Eintrags und stellst eine ältere Version per Diff-Auswahl wieder her.
- **JSON-Overlay:** Für Technikfälle bearbeitest du den Roh-Datensatz direkt (Systemfelder-Einfügung inklusive).
## Widgets
Der `WidgetDispatcher` wählt das Eingabeelement aus `meta.widget` — sonst aus dem Feldtyp. Alle 25 Schema-Widgets aus [Felder und Widgets](/projekt/felder-widgets/) werden unterstützt (`text`, `textarea`, `number`, `checkbox`, `switch`, `select`, `selectArray`, `chipArray`, `date`, `datetime`, `file`, `image`, `json`, `richtext`, `html`, `foreignKey`, `foreignKeyChipArray`, `foreignFile`, `foreignMedia`, `pagebuilder`/`pageBuilder`, `templateProps`, `containerLessObject`, `containerLessObjectArray`, `color`).
| Eingabe | Widgets | Hinweis |
|---|---|---|
| Kurztext | `text` | Eine Zeile, Standard für `string` |
| Langtext | `textarea`, `richtext`, `html` | Editor mit Werkzeugleiste bei `richtext` |
| Zahl | `number` | Auch für `number[]` |
| Ja/Nein | `checkbox`, `switch` | Reine Flags brauchen kein Widget |
| Auswahl | `select`, `selectArray`, `chipArray` | Optionen aus `meta.choices` |
| Datum | `date`, `datetime` | Zeitanteil nur bei `datetime` |
| Datei/Bild | `file`, `image` | Vorschau plus [Bildeditor](/admin/medien/) |
| Struktur | `json`, `templateProps`, `containerLessObject(-Array)` | Freies JSON ohne `subFields` |
| Seite | `pagebuilder` | Blöcke mit Typ-Umschalter und `dependsOn`-Feldern |
| Farbe | `color` | Farbwähler |
| Verweis | `foreignKey`, `foreignKeyChipArray`, `foreignFile`, `foreignMedia` | Siehe unten |
<Aside type="note">
Felder mit `meta.dependsOn.eval` blendet der Editor per JavaScript ein oder aus (z. B. Block-Optionen nur beim passenden Blocktyp). `meta.hide: true` versteckt Technikfelder dauerhaft.
</Aside>
## Fremdschlüssel und Drill-Down
Verweisfelder (`foreignKey` und Verwandte) öffnen einen **Auswahl-Dialog**: Suche, Sortierung und Blättern wie in der [Liste](/admin/ansichten-listen/), aber gegen die Zielsammlung. Du übernimmst einen Eintrag oder legst ihn inline neu an — Vorbelegung aus dem Filter inklusive.
- **Mehrfach-Verweise** (`foreignKeyChipArray`) sammeln mehrere Ziele als Chips.
- **Datei-Verweise** (`foreignFile`, `foreignMedia`) zeigen auf die [Mediathek](/admin/medien/); die Auflösung beim Lesen per `lookup` steht unter [Lesen und suchen](/projekt/abfragen-lesen/).
- **Verlinkter Editor:** Aus dem Dialog öffnest du den Zieleintrag in einem gestapelten Drawer und bearbeitest ihn, ohne deinen Eintrag zu verlassen.
- **Drill-Down:** Tiefe Objekt-Arrays öffnet der Editor als Ebene mit **Brotkrumen-Navigation** — ein Klick auf eine Ebene springt zurück, `Esc` geht eine Stufe hoch.
## Mehrsprachigkeit
Zwei Muster — beide aus Projekt-Sicht konfiguriert ([Felder und Widgets](/projekt/felder-widgets/), [Navigation und Sprachen](/projekt/navigation-i18n/)):
<Tabs>
<TabItem label="Sprach-Objekt">
Felder wie `alt.de` / `alt.en` liegen als Unterfelder nebeneinander. Der Editor gruppiert sie mit Sprach-Reitern (`I18nFieldWrapper`, `I18nLanguageTabs`) — du wechselst die Sprache pro Feldgruppe.
</TabItem>
<TabItem label="Einträge koppeln">
Jede Sprache ist ein eigener Eintrag mit gleichem Gruppenschlüssel (`meta.i18n.entry`: `languageField`, `groupField`). Die **Übersetzungsleiste** über dem Formular zeigt Geschwister (anklickbar), fehlende Sprachen (per `+` anlegbar, optional mit KI-Übersetzung) und das Entkoppeln aus der Gruppe.
</TabItem>
</Tabs>
Sprachwechsel-Tastenkürzel: `Strg + L` (zyklisch durch die Inhaltssprachen).
## Tastatur
| Kürzel | Wirkung |
|---|---|
| `Strg + S` | Speichern |
| `Strg + Z` / `Strg + Umschalt + Z` | Rückgängig / Wiederholen |
| `Strg + F` | Feldsuche im Formular |
| `Strg + L` | Inhaltssprache wechseln |
| `Strg + Umschalt + F` | Suchmodus der Verweis-Dialoge wechseln |
| `Umschalt + N` | Eintrag duplizieren |
| `Esc` | Drill-Down-Ebene zurück / Dialog schließen |
| `?` | Tastaturhilfe öffnen |
<Aside type="tip">
Drücke `?` im Editor — die Hilfe listet alle Kürzel inklusive Listen-Kürzel (`s`, `f`, `x`, `o`, `v`, `p` + Ziffer).
</Aside>
## Häufige Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Speichern blockiert, rote Felder | Pflichtfeld leer oder `pattern`/`in`-Regel verletzt | Tooltip am Speichern-Knopf lesen, erstes rot markiertes Feld füllen |
| Verweis lässt sich nicht wählen | Zieleintrag ohne Leserecht oder Filter zu eng | Filter im Dialog leeren, Rechte unter [Rechte und Tokens](/projekt/rechte-tokens/) prüfen |
| Übersetzung fehlt in der Leiste | Kein Gruppenschlüssel oder Sprache nicht konfiguriert | `translationKey`/`lang` prüfen, fehlende Sprache per `+` anlegen |
| Änderung „verschwunden“ | Drawer geschlossen ohne Speichern | Auf Änderungs-Warnung achten, erst Drawer-, dann Haupt-Speichern |
<!-- Screenshot-Platzhalter (T-SHOTS): Editor mit Formular, Seitenleiste und Übersetzungsleiste, Alt-Text deutsch, max. 200 KB unter src/assets/. -->
Verifiziert am 2b091d89 2026-10-13 (tibi-admin-nova)
+85
View File
@@ -0,0 +1,85 @@
---
title: 'Exporte'
description: 'CSV, XLSX, JSON aus Listen, vordefinierte und eigene Skript-Exporte.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge } from '@astrojs/starlight/components';
Du exportierst Einträge einer Sammlung im **tibi-admin** (Verwaltungs-Backend, Release Nova) direkt aus der [Liste](/admin/ansichten-listen/): als Datei für Tabellen­kalkulation, Datenaustausch oder eigene Berichte. Exportiert wird, was du siehst — Filter und Suche der Liste gelten auch für den Export.
## Listen-Export
<Steps>
1. Liste filtern und sortieren, bis sie zeigt, was du brauchst.
2. Export-Knopf über der Liste klicken.
3. Format wählen: `JSON`, `CSV` oder `XLSX`.
4. Spalten per Checkbox anhaken (Vorauswahl: sichtbare Tabellenspalten; `object[]`-Felder erscheinen einmal und werden beim Schreiben aufgefächert).
5. Herunterladen — der Dateiname enthält Sammlung und Datum.
</Steps>
| Format | Technik | Wofür |
|---|---|---|
| `JSON` | Rohdatensätze als Datei | Weiterverarbeitung, Sicherung, Import anderswo |
| `CSV` | Tabellenzeilen aus der `xlsx-js-style`-Bibliothek | Excel-, Numbers- oder Skript-Import |
| `XLSX` | Arbeitsmappe aus derselben Bibliothek (Zell-Stile möglich) | Formatierte Weitergabe an Menschen |
<Aside type="note">
`xlsx-js-style` (SheetJS-Fork mit Zell-Stilen) ist die einzige Tabellen-Bibliothek im Admin — beide Tabellenformate kommen aus ihr (`json_to_sheet` für XLSX, `sheet_to_csv` für CSV).
</Aside>
- **Große Mengen:** Vor dem Laden schätzt der Admin die Trefferzahl über `GET …/<sammlung>/_count` (gleicher Filter wie die Liste, Suche als Obergrenze) und warnt dich vor sehr großen Exporten.
- **Dynamische Spalten:** Wachsen Arrays unterschiedlich lang, fragt der Dialog nach, ob er die Spalten auffächern soll — brichst du ab, bleibt der Dialog offen.
## Vordefinierte Skript-Exporte
Stehen im Export-Dialog eigene Reiter **vor** dem Standard-Tab, hat das Projekt fertige Berichte in `collection.meta.exports` hinterlegt (Name, Datei, optional Dateiname mit `{date}`-Platzhalter). Ein Klick führt das Skript mit dem aktuellen Listenfilter aus — inklusive Fortschrittsbalken.
```yaml title="Export-Definition in der Sammlungs-Config"
# Beispiel-Eintrag in der Sammlungs-Config
exports:
- name: bestellreport
label: { de: "Bestellreport", en: "Order report" }
file: "_/assets/exports/reports.mjs"
filename: "bestellreport-{date}.xlsx"
```
## Eigenes Skript
Projekt-Entwickler legen eigene Exporte als ES-Modul unter `meta.exports[].file` ab (Pfad mit `_/`-Präfix = projekt-relativ, empfohlen). Das Modul exportiert **eine Standard-Funktion**, die Kontext (Filter, Auswahl, Fortschritt) bekommt und genau ein Ergebnis zurückgibt:
| Rückgabe | Inhalt |
|---|---|
| `workbook` | XLSX-Arbeitsmappe (wird als `.xlsx` gespeichert) |
| `blob` | Fertiger `Blob` (z. B. aus Fremd-Lib) |
| `text` | Text wie CSV oder HTML |
| `bytes` | Binärdaten wie PDF (`Uint8Array`/`ArrayBuffer`) |
Dazu Pflicht: `filename` und `contentType` (z. B. `text/csv`, `application/pdf`) — ohne `contentType` wirft der Admin einen Fehler. Genau ein Payload pro Ergebnis.
```js title="reports.mjs (vereinfacht)"
// reports.mjs — stark vereinfacht
export default async function (ctx) {
const rows = await ctx.db.loadAll({ filter: ctx.filter });
ctx.progress(1, 1, "Fertig");
const wb = ctx.xlsx.utils.json_to_sheet(rows);
return { workbook: { SheetNames: ["Report"], Sheets: { Report: wb } }, filename: "bestellreport-{date}.xlsx", contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" };
}
```
Details für Entwickler (Kontext-Felder, Dateinamen-Platzhalter, Sanitizing) stehen in der Admin-Repo-Doku `custom-export-script.md` — im Zweifel gilt der Code (`utils/load-custom-export.ts`, `utils/custom-export-context.ts`, `utils/export.ts`).
## Häufige Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Export enthält „falsche“ Einträge | Listenfilter (Reiter, Feld-Filter, Suche) enger als gedacht | Filter in der Liste prüfen — der Export übernimmt sie eins zu eins |
| Spalten-Warnung erscheint | Arrays ungleicher Länge brauchen aufgefächerte Spalten | Auffächern bestätigen oder Spaltenauswahl verkleinern |
| Eigener Reiter fehlt | `meta.exports`-Eintrag ohne `name` oder `file` | Projekt-Config ergänzen (beide Pflicht), Admin neu laden |
| Skript-Fehler im Dialog | Modul ohne Standard-Export oder Laufzeitfehler | Browser-Konsole lesen, Modul-Export und Rückgabeformat prüfen |
| Leere Datei trotz Treffern | Leserecht auf Zielfelder fehlt | Rechte unter [Rechte und Tokens](/projekt/rechte-tokens/) klären |
<!-- Screenshot-Platzhalter (T-SHOTS): Export-Dialog mit Formatwahl und Spaltenliste, Alt-Text deutsch, max. 200 KB unter src/assets/. -->
Verifiziert am 2b091d89 2026-10-13 (tibi-admin-nova)
+134
View File
@@ -0,0 +1,134 @@
---
title: 'KI-Funktionen'
description: 'Chat, Zusammenfassen, Stimme und Werkzeuge im Admin: was die KI kann und wo ihre Grenzen liegen.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Die KI im **tibi-admin** (Verwaltungs-Backend, Release Nova) hilft dir beim Redigieren: Sie chattet im Kontext der aktuellen Ansicht, fasst lange Verläufe zusammen, hört zu (STT) und liest vor (TTS). **Wichtig vorab: Im Frontend wählst du kein Modell** — alle Modellentscheidungen trifft der Proxy (`config/ai-proxy.config.json`); das Frontend ruft nur Endpunkte aus `frontend/assets/system.config.json` auf.
## Chat
<Steps>
1. **„Nova öffnen“** klicken (`AiButton.svelte`) — der Chat (`AiChat.svelte`) öffnet sich mit Verlauf (`AiConversationList.svelte`). Begrüßung: „Hallo! Ich bin Nova. Wie kann ich dir helfen?“
2. Frage in **„Nachricht eingeben…“** stellen und **„Senden“** drücken — die KI sieht den aktuellen UI-Kontext (kompaktes JSON-Snapshot, < 500 Token).
3. Antwort prüfen: Werkzeug-Schritte zeigt `AiToolStatus.svelte`, Details der Aufrufe `AiToolInspector.svelte`.
4. Weiterfragen oder über **„Neuer Chat“** von vorn beginnen (`start_new_chat`-Werkzeug).
</Steps>
Was die KI dabei sieht (`aiController.ts`, `AiChat.svelte`): Neben dem UI-Snapshot hängt der Chat deine Aktionen außerhalb des Chats an die nächste Nachricht an (`[User actions performed outside of chat] … [User message] …`) und markiert die erste Nachricht mit `[lang=de]` (bzw. deiner Sprache). Manche Provider verlangen die Systemnachricht genau einmal vorn — deshalb wandern Aktionen in die Benutzer-Nachricht, nicht in eine zweite Systemnachricht.
| Baustein | Datei | Aufgabe |
|---|---|---|
| Verlauf | `lib/aiChatHistory.ts` | Gespräche in `localStorage` (`tibi-ai-conversations`, weiches Limit ~4 MB, älteste zuerst raus) |
| Steuerung | `lib/aiController.ts` | Kontext lesen, Aktionen ausführen (nicht destruktiv; Heikles nur mit deiner Bestätigung), Cursor-Animation mit Tempo 0–3 |
| Werkzeug-Definitionen | `lib/aiTools.ts` | Funktionsaufrufe für den Chat (nächster Abschnitt) |
| Schätzung | `lib/aiTokenEstimator.ts` | Heuristik ~4 Zeichen ≈ 1 Token, nur für Vorab-Entscheidungen |
**„Token-Status“** (Knopf im Chat, `showTokenStatus`): Das Popover zeigt **„Kontext-Auslastung“** (Prozent mit Farbstufe: grün < 40 %, gelb < 70 %, orange < 85 %, rot darüber — `getContextStatus`), **„Letzter Turn“** (**„Prompt-Tokens“**, **„Completion-Tokens“**, **„Gesamt-Tokens“**, **„Geschätzte Kosten“**), **„Konversation gesamt“** (inkl. Zusammenfassungs-, Spracheingabe- und Sprachausgabe-Kosten) sowie **„Modell“** und **„Kontextfenster“**. Gezählt wird zweigleisig: Die Schätzung (`estimateTokens`, `estimateSystemOverhead`) steuert Vorab-Entscheidungen, die echten Zähler kommen aus der Stream-Antwort (`stream_options`) und landen in **„Geschätzte Kosten“** über die Anzeigetabelle `TEXT_TOKEN_PRICING` — reine Anzeige, keine Abrechnung.
Das **„Kontextfenster“** liest der Chat aus dem Modellkatalog (Endpunkt `/api/ai/models`, `configured.chat.contextWindow`); antwortet der Katalog nicht, gelten 96 000 Token (`MODEL_CONTEXT_WINDOW`). Aktuell meldet der Proxy 262 144 (siehe `config/ai-proxy.config.json`, `chat.contextWindow`).
Tempo der Cursor-Animation: **„Nova-Speed“** mit **„Hyperspeed“** (0, alles sofort), **„Schnell“** (1, Standard), **„Mittel“** (2), **„Langsam“** (3) — als Befehle (**„Nova-Speed: …“**) und Kennung `ai-animation-speed` in `localStorage`. Bei zu vielen Schritten stoppt Nova von selbst: „Automatisch gestoppt – zu viele aufeinanderfolgende Aktionen. Nova wurde nach {count} Schritten angehalten …“ — dann Anfrage neu formulieren oder Felder manuell prüfen.
## Zusammenfassen
Wächst ein Gespräch Richtung Kontextgrenze, komprimiert `lib/aiSummarizer.ts` in zwei Stufen:
| Stufe | Schwelle | Verhalten |
|---|---|---|
| Verdichten (kostenlos, synchron) | Mehr als 12 Werkzeug-Aktivitäten | `condenseHistoricalToolMessages`: abgeschlossene alte Werkzeug-Gruppen werden zu je einer Historie-Notiz (`[HISTORY-CONDENSED: …]`, Rolle `user`), die neuesten 12 Aktivitäten bleiben roh |
| Zusammenfassen per LLM | 60 % des Fensters (`SUMMARIZE_THRESHOLD`) | Ältere Nachrichten per LLM zusammenfassen, neueste ~6 Nachrichten (max. ~8 000 Token) roh behalten |
| 85 % (`HARD_TRUNCATE_THRESHOLD`) | Hart kürzen: älteste Nachrichten verwerfen |
- Reserviert sind 10 000 Token für System-Prompt, Werkzeug-Definitionen und Antwort (`RESERVED_TOKENS`).
- Zusammenfassungs-Modell: `proxy-managed` — auch hier entscheidet der Proxy, nicht das Frontend. Der Aufruf geht an den Zusammenfassungs-Endpunkt mit Profil `summarizer` und liest einen SSE-Strom (inkl. Zähler); Antwort leer oder Netzfehler → Rückfall auf hartes Kürzen.
- Hartes Kürzen hält Werkzeug-Paare zusammen: Verwaiste `tool`-Antworten und `assistant`-Nachrichten ohne vollständige Ergebnisse fliegen raus (`hardTruncate`, `sanitizeWireMessages`) — sonst würde die API den Turn ablehnen.
- UI: Währenddessen steht **„Kontext wird zusammengefasst…“**, danach **„Kontext zusammengefasst“** bzw. **„Zusammenfassung aktiv“** mit **„{count} Nachrichten komprimiert · ~{tokens} Tokens gespart“**.
- Themenwechsel: Schlägt Nova einen neuen Chat vor (**„Neuen Chat starten?“** — **„Neuen Chat starten“** oder **„Nein, weiter“**), fasst `summarizeForTransfer` den ganzen Verlauf zusammen und stellt deine letzte Nachricht wörtlich unter „LATEST REQUEST“. Im neuen Chat erscheint das als **„Kontext aus vorherigem Chat“** (**„{count} Nachrichten aus vorherigem Chat · ~{tokens} Tokens übertragen“**).
## Stimme
<Tabs>
<TabItem label="Diktieren (STT)">
**„Spracheingabe“** (`lib/aiVoiceInput.ts`, `VoiceRecorder`): Sprich deinen Prompt statt ihn zu tippen.
1. Mikro-Knopf drücken — zuerst versucht der Browser **native Spracherkennung** (Status **„Hört auf diesem Gerät“**, Sprache aus `<html lang>`, Standard `de-DE`).
2. Klappt das nicht, zeichnet der Admin Audio auf (**„Nimmt für KI-Transkription auf“**) und schickt es an `/api/ai/transcribe` (Proxy-Modell `large-v3-turbo`, Antwort als Text — Status **„Transkribiert mit KI“** bzw. **„Erkennt auf diesem Gerät“**).
3. Das Transkript landet im Chat-Eingabefeld (**„Wird transkribiert…“** währenddessen); Dauer und Quelle (`browser`/`api`) stehen dabei. **„Aufnahme stoppen“** beendet manuell, **„Abbrechen“** verwirft.
Grenzen: Stille beendet automatisch nach 3 s (mobil 3,5 s, frühestens nach 600 ms Aufnahme); Schnipsel unter ~1 000 Byte werden ohne Transkription verworfen. Kostenkopf der Antwort (`X-AI-Cost-Usd`, `X-AI-Model`) zählt in **„Konversation gesamt“** unter **„Spracheingabe“**.
</TabItem>
<TabItem label="Vorlesen (TTS)">
Antworten anhören statt lesen (`lib/aiTTS.ts`, Endpunkt `/api/ai/tts`) — Knopf **„Vorlesen“**, Stopp über **„Wiedergabe stoppen“** (**„Audio wird geladen…“** während des Ladens):
| Einstellung | Wert (`system.config.json`) |
|---|---|
| Stimmen | `eva-de` (Standard), `kerstin-de`, `ramona-de` |
| Tempo | **„Geschwindigkeit“** 0,5–2,0, Schritt 0,1, Standard **1,3** (**„Zurücksetzen“** stellt den Standard wieder her) |
| Format | MP3 (`responseFormat: mp3`) — Opus im WebM, wenn dein Browser es kann (`preferredTTSFormat`) |
Verhalten: Quelle zuerst Browser-**„Systemstimme auf diesem Gerät“** (passend zu deiner Dokumentsprache, Standard `de-DE`), sonst **„KI-Stimme“** (Proxy-Modell `qwen3-tts-0.6b-base`) — Anzeige **„Stimme: {voice}“**. Beim Streamen spricht Nova satzweise mit: erster Block ab ~60 Zeichen, danach ~120-Zeichen-Bündel, bis zu 2 parallele Anfragen, lückenlos aneinandergereiht. Schalter **„Sprachausgabe“** / **„Sprachausgabe aus“**; Zeichen und Kosten meldet der Server per Kopf (`X-AI-Characters`, `X-AI-Cost-Usd`) und landen unter **„Sprachausgabe“** in **„Konversation gesamt“**.
</TabItem>
</Tabs>
Zusatz: **Vision** (`/api/ai/vision`) beschreibt Bilder für CMS-Felder („erfinde keine nicht sichtbaren Fakten“); **Übersetzung** (`/api/ai/chat` mit System-Prompt) hält das angeforderte Ausgabeformat exakt ein.
## Werkzeuge
Die KI ruft UI-Werkzeuge statt zu raten (`aiTools.ts` → `aiController.ts`). Labels wörtlich aus `de.json` (`admin.ai.tools.*`); Status dazu: **„Arbeitet…“** währenddessen, danach **„1 Aktion ausgeführt“** bzw. **„{count} Aktionen ausgeführt“**, **„Details“** öffnet den **„Tool-Inspector“** (**„Tool-Aufruf“** / **„Tool-Rückgabe“**, **„Formatiert“** / **„Roh“**, **„Alle Schritte kopieren“**):
| Gruppe | Werkzeuge (UI-Name) |
|---|---|
| Navigation | `navigate` (**„Navigieren“**), `guide_to` (**„Geführte Tour“**), `list_projects` (**„Projekte auflisten“**), `select_project` (**„Projekt öffnen“**), `select_collection` (**„Collection öffnen“**) |
| Liste | `search` (**„Suchen“**), `set_search_mode` (**„Suchmodus wechseln“**), `set_filter` (**„Filter setzen“**), `clear_filters` (**„Filter löschen“**), `sort_by` (**„Sortieren“**), `set_page` (**„Seite wechseln“**), `set_page_size` (**„Seitengröße ändern“**) |
| Auswahl | `select_records` (**„Datensätze auswählen“**), `select_all` (**„Alle auswählen“**), `deselect_all` (**„Auswahl aufheben“**), `open_collection_record` (Datensatz öffnen), `set_view_mode` (**„Ansicht wechseln“**) |
| Stapel | `open_bulk_edit` (**„Massenbearbeitung“**), `prepare_bulk_delete` (**„Löschung vorbereiten“**) |
| Eintrag & Formular | `get_entry_context` (**„Eintrag-Kontext abrufen“**), `fill_field` (**„Feld ausfüllen“**), `fill_form` (**„Formular ausfüllen“**), `focus_field` (**„Feld fokussieren“**), `save_entry` (**„Eintrag speichern“**), `cancel_entry` (**„Bearbeitung abbrechen“**), `create_entry` (**„Neuen Eintrag anlegen“**), `duplicate_entry` (**„Eintrag duplizieren“**) |
| Array-Felder | `add_array_item` (**„Array-Element hinzufügen“**), `remove_array_item` (**„Array-Element entfernen“**), `duplicate_array_item` (**„Array-Element duplizieren“**), `reorder_array_items` (**„Array-Elemente sortieren“**), `clear_array` (**„Array leeren“**) |
| Verknüpfungen | `search_foreign_options` (**„Verknüpfte Einträge durchsuchen“**), `create_foreign_entry` (**„Verknüpften Eintrag erstellen“**) |
| Navigation (Inhalt) | `open_navigation_node` (**„Navigationsknoten öffnen“**), `add_navigation_node` (**„Navigationsknoten hinzufügen“**), `move_navigation_node` (**„Navigationsknoten verschieben“**), `delete_navigation_node` (**„Navigationsknoten löschen“**) |
| Medien | `upload_media` (**„Medien hochladen“**), `quick_upload_all` (**„Alle schnell hochladen“**), `edit_image` (**„Bild bearbeiten“**), `inline_edit_title` (**„Medieneintrag öffnen“**), Warteschlange: `list_queue_items` (**„Warteschlange anzeigen“**), `edit_queue_item` (**„Element bearbeiten“**) |
| Verlauf | `get_audit_history` (**„Verlauf abrufen“**), `list_audit_log` (**„Audit-Log anzeigen“**), `restore_from_audit` (**„Aus Audit übernehmen“**) |
| Oberfläche | `toggle_sidebar` (Seitenleiste, Tool-Name als Anzeige), `open_field_search` (Feldsuche, Tool-Name als Anzeige), `set_theme` (**„Theme wechseln"**), `switch_language` (**„Sprache wechseln"**), `highlight_element` (**„Element hervorheben"**), `explain_ui` (**„UI erklären"**), `list_ui_elements` (**„UI-Elemente auflisten"**), `click_element` (**„Element klicken"**), `drill_navigate` (Ansicht lesen, Tool-Name als Anzeige) |
| Daten lesen | `get_collection_records` (Datensätze lesen, Tool-Name als Anzeige), `get_collection_schema` (Schema lesen, Tool-Name als Anzeige), `list_users` (**„Benutzer auflisten"**) |
| Chat | `start_new_chat` (**„Neuer Chat“**), `run_tool_batch` (**„Batch ausführen“**) |
Stapel-Limits (`aiTools.ts`): `run_tool_batch` fasst bis zu 40 ändernde Schritte (100 bei rein lesenden wie **„Kontext abrufen“**, **„Projekte auflisten“**, **„Benutzer auflisten“**) — darüber verweigert der Batch. `get_context` (**„Kontext abrufen“**) und `get_entry_context` lesen gezielt nach: Standardablauf ist erst Eintrag öffnen, dann `get_entry_context`, dann die Feld-Updates möglichst als ein Batch.
<Aside type="note">Alle Aktionen sind nicht destruktiv angelegt; `prepare_bulk_delete` bereitet nur vor und `restore_from_audit` (**„Aus Audit übernehmen"**) fragt vorher bei dir nach. Zwei Debug-Ansichten: `AiDebugContext.svelte` (Befehlspalette: **„Debug: KI-Kontext anzeigen"**) zeigt, welchen Kontext die KI gerade sieht; `AiVisionOverlay.svelte` (Befehlspalette: **„Debug: KI-Vision"**, Schalter **„Debug: KI-Vision (steuerbare Elemente anzeigen)"**) legt ein Röntgen-Overlay über alle steuerbaren Elemente mit ihren Kennungen.</Aside>
## Grenzen und Kosten
Wichtig zur Einordnung: Der Admin-Chat läuft über den Proxy (`/api/ai/chat`, `/api/ai/vision`, `/api/ai/transcribe`, `/api/ai/tts`) — **nicht** über `POST /api/v1/llm/chat` am tibi-server. Die Benutzer-/Org-Budgets mit `429` aus [LLM nutzen](/projekt/llm-nutzen/) greifen daher hier nicht; was hier begrenzt, steht in `config/ai-proxy.config.json` und `frontend/assets/system.config.json`:
| Punkt | Stand |
|---|---|
| Modellwahl | Gibt es im Frontend nicht — Proxy-Config entscheidet (Chat, Zusammenfassen, Übersetzung, Vision je eigenes Modell/Fenster) |
| Preise im Proxy | Aktuell `0` (interne Test-Modelle: alle `pricing`-Felder `0`); die Schätztabelle im Frontend (`TEXT_TOKEN_PRICING`) dient nur der Anzeige im **„Token-Status“** |
| Fenster & Antwortlänge | Chat 262 144 / max. 12 000 Token, Zusammenfassen und Übersetzung je 262 144 / 6 000, Vision 32 768 / 6 000 |
| Verlauf | Nur lokal im Browser (`localStorage`: `tibi-ai-conversations`, weich ~4 MB — älteste zuerst raus; Single-Chat-Notbremse: nur letzte 4 Wire-Nachrichten bleiben) |
| Stapel | `run_tool_batch`: max. 40 ändernde bzw. 100 rein lesende Schritte |
| Sprache | 3 s Stille beendet (mobil 3,5 s), Schnipsel < ~1 KB verfallen; TTS-Bündel ab ~60/120 Zeichen |
| Offline | Ohne Server keine KI — Chat, STT, TTS brauchen ihre Endpunkte |
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Chat antwortet nicht | Endpunkt `/api/ai/chat` unerreichbar oder Proxy down | [Admin einrichten](/admin/setup/) prüfen, später erneut versuchen |
| **„KI-Dienst ist nicht erreichbar. Bitte versuche es später erneut.“** | Endpunkt fehlt (404) oder Proxy liefert JSON statt Nutzdaten | [Admin einrichten](/admin/setup/) prüfen |
| **„KI-Dienst nicht konfiguriert. Bitte kontaktiere den Administrator.“** | 401/403 am Proxy (Schlüssel/Config) | Administrator ansprechen |
| **„Rate-Limit erreicht. Bitte warte einen Moment und versuche es erneut.“** | 429 am Proxy | Kurz warten, erneut versuchen |
| **„Der KI-Dienst ist vorübergehend nicht erreichbar. Bitte versuche es später erneut.“** | 5xx am Proxy | Später erneut versuchen |
| **„Mikrofon-Zugriff verweigert. …“** | Browser-Recht fehlt | Mikrofon in den Browser-Einstellungen freigeben |
| **„Transkription fehlgeschlagen: {error}“** | Rate-Limit, Netz oder Proxy-Fehler | Kürzeren Abschnitt sprechen, erneut versuchen |
| Stimme klingt falsch | Andere Stimme aktiv | Stimme in den KI-Einstellungen zurück auf `eva-de` setzen |
<!-- Screenshot-Platzhalter (T-SHOTS): geöffneter KI-Chat mit Werkzeugstatus und Mikro-Knopf. -->
Verifiziert am 2b091d89 2026-10-16 (tibi-admin-nova: AiChat/AiButton/AiConversationList/AiToolStatus/AiToolInspector/AiDebugContext, aiChatHistory/aiController/aiTools/aiTokenEstimator/aiVoiceInput/aiSummarizer/aiTTS/aiModelCatalog, config/ai-proxy.config.json, frontend/assets/system.config.json, de.json admin.ai + Befehle)
+36
View File
@@ -0,0 +1,36 @@
---
title: Admin-Konzept
description: Was tibi-admin ist, wie es läuft und wie es zum Server passt.
---
import { Card, CardGrid } from '@astrojs/starlight/components';
> **Handseite.** Wird vom Sync nie überschrieben (`keep`).
**tibi-admin** ist das Verwaltungs-Backend — die Bedienoberfläche zum Tibi-Server (aktuelles Release **Nova**). Code-Repo: `tibi-admin-nova` — das ist das einzig gültige Repo, alte `tibi-admin`-Repos werden ignoriert. Projekt-Websites (kleine Websites, Shops, Intranet-Anwendungen aus `tibi-svelte-starter`) sind eigenständig und laufen getrennt davon.
- Svelte-SPA als statisches Bundle, läuft lokal im Mock-Modus (Projekt `t1`) und in Staging/Live hinter Reverse Proxy gegen den echten tibi-server.
- Laufzeitwerte in `frontend/assets/system.config.json`, Proxy-Ziele/Modelle in `config/ai-proxy.config.json`.
- Screens: `frontend/src/lib/components/admin/` (Hub, Orgs, Teams, User, Projekte) und `components/entry/` (Editor, Widgets, Drawers, Preview).
## Wo finde ich was?
<CardGrid stagger>
<Card title="Einrichten">
Docker-Stack, Mock-Modus, URLs. [Lesen](/admin/setup/).
</Card>
<Card title="Anpassen">
Ansichten, Widgets, Fremdschlüssel, Exporte, Skripte. [Lesen](/admin/customizing/).
</Card>
<Card title="Arbeiten mit Inhalten">
[Ansichten und Listen](/admin/ansichten-listen/), [Eintrags-Editor](/admin/eintrags-editor/), [Medien](/admin/medien/), [Exporte](/admin/exporte/).
</Card>
<Card title="Steuern und Rechte">
[Aktionen](/admin/aktionen/), [Benutzer und Rechte](/admin/benutzer-rechte/), [KI-Funktionen](/admin/ki-funktionen/).
</Card>
<Card title="Anwender-Anleitung">
Bereich [Anwender](/anwender/anmelden/): Bedienung des Verwaltungs-Backends für Redakteure und Betreiber — nicht Anleitung für Besucher der Projekt-Websites.
</Card>
</CardGrid>
Verifiziert am 2b091d89 2026-10-13 (tibi-admin-nova).
+83
View File
@@ -0,0 +1,83 @@
---
title: 'Medien im Admin'
description: 'Raster, Upload-Warteschlange, Bildeditor, Vorschau aus Admin-Sicht.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge } from '@astrojs/starlight/components';
Bilder, PDFs und andere Dateien verwaltest du im **tibi-admin** (Verwaltungs-Backend, Release Nova) in einer Medien-Sammlung — meist `medialib` mit `viewHint.media`. Dieselben Werkzeuge stecken auch in Datei-Feldern des [Eintrags-Editors](/admin/eintrags-editor/). Projekt-Grundlagen (Pfade, Größen, Formate) stehen unter [Dateien und Bilder](/projekt/dateien-bilder/).
## Raster
Die Medienansicht zeigt Kacheln mit Vorschaubild, Titel und Auswahl-Status. Darüber liegen Suche, Sortierung, Filter und Blättern wie in jeder [Liste](/admin/ansichten-listen/).
- **Klick** öffnet die Vollbild-**Vorschau** (Lightbox): Blättern per Pfeiltasten und Knöpfe, Download-Knopf, PDF-Vorschau per eingebettetem Rahmen.
- **Mehrfachauswahl** per Checkbox oder `Leertaste` — danach Massenbearbeitung oder Löschen wie in der Tabellenansicht.
- **Titel-Erkennung:** Fehlt eine explizite `autoFill`-Konfiguration, rät der Admin das Titelfeld aus üblichen Namen (`title`, `name`, `titel`, `label`, `bezeichnung`).
- **Hervorhebung:** Suchtreffer markiert das Raster im passenden Feld.
## Upload
<Steps>
1. Dateien auf das Raster ziehen oder den Upload-Knopf klicken (Mehrfachauswahl möglich).
2. Pro Datei füllt der Admin Titel und Alternativtext aus dem Dateinamen vor (`autoFill`-Konfiguration der Sammlung).
3. Die **Warteschlange** lädt mit Fortschrittsbalken — einzeln sofort oder alle als Stapel.
4. Nach dem Hochladen lädt das Raster neu; die neuen Einträge sind direkt verlinkbar.
</Steps>
- **Dateinamen-Import:** Die Warteschlange erkennt Titel aus Dateinamen und schlägt Alternativtexte vor.
- **KI-Alt-Text:** Ist er für die Sammlung konfiguriert, läuft die Generierung pro Warteschlangen-Eintrag mit Statusanzeige (wartend, laufend, abbrechbar).
- **Inline-Anlage:** Aus Datei-Feldern heraus legst du Medien-Datensätze im Drawer an, ohne den Editor zu verlassen.
- **Grenzen:** Erlaubte Typen und maximale Größe kommen aus dem Feld-Validator (`accept`, `maxFileSize`, z. B. `"50MB"` in `medialib.yml`) — Schema-Details unter [Felder und Widgets](/projekt/felder-widgets/).
## Bildeditor
Den Editor öffnest du aus der Kachel oder aus Datei-Feldern (`Bild bearbeiten`). Er arbeitet rein im Browser (Canvas/CSS, keine Zusatzbibliothek) und schreibt das Ergebnis ins Dateifeld zurück.
| Werkzeug | Was du einstellst |
|---|---|
| Zuschnitt | Freies oder festes Seitenverhältnis, pixelgenau |
| Drehen/Spiegeln | 90°-Schritte, horizontal/vertikal (Zuschnitt wird dabei zurückgesetzt) |
| Anpassen | Helligkeit, Kontrast, Sättigung mit Live-Vorschau |
| Fokuspunkt | Klickpunkt für Ausschnitt-Steuerung in der Projekt-Website |
| Skalieren | Zielgröße in Pixeln |
| Palette | Dominante Farben prüfen und pflegen |
Welche Werkzeuge verfügbar sind, steuert die Projekt-Konfiguration (`crop`, `rotate`, `flip`, `scale`, `focalPoint`, `adjust`, `palette`).
## Einbetten
Aus Redakteurs-Sicht gibt es drei Wege, eine Datei in Inhalte zu bringen:
<Tabs>
<TabItem label="Bild-Feld">
`foreignMedia`-Felder (z. B. `blocks.heroImage.image`) verweisen auf die Mediathek. Wähle den Eintrag im Verweis-Dialog — die Projekt-Website löst ihn per `lookup` auf ([Lesen und suchen](/projekt/abfragen-lesen/)).
</TabItem>
<TabItem label="Datei-Feld">
`file`- und `image`-Felder halten den Upload direkt am Eintrag (Klick oder Drag-and-drop, Vorschau, Bildeditor). Erlaubte Typen liest der Admin aus `validator.accept`.
</TabItem>
<TabItem label="Richtext">
Bilder im Fließtext verwaltet das `richtext`-Widget des Editors (siehe [Eintrags-Editor](/admin/eintrags-editor/)).
</TabItem>
</Tabs>
## Häufige Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Upload abgelehnt | Typ nicht in `accept` oder Datei über `maxFileSize` | Kleinere oder passende Datei wählen, ggf. Projekt-Konfiguration anpassen |
| Vorschaubild fehlt | Kein Bildfeld erkannt oder Datei kein Bild | Titel- und Dateifeld der Sammlung prüfen, PDF öffnet sich in der Vorschau statt als Bild |
| Bild-Einbettung bleibt leer | Verweis ohne `lookup` gelesen oder Eintrag nicht öffentlich | `lookup` auf `medialib` prüfen, `active`-Status und Rechte unter [Rechte und Tokens](/projekt/rechte-tokens/) klären |
| Editor-Änderung weg | Dialog ohne Übernehmen geschlossen | Erst im Bildeditor übernehmen, dann den Eintrag speichern |
<!-- Screenshot-Platzhalter (T-SHOTS): Medienraster mit Warteschlange und Bildeditor-Dialog, Alt-Text deutsch, max. 200 KB unter src/assets/. -->
Verifiziert am 2b091d89 2026-10-13 (tibi-admin-nova)
+85
View File
@@ -0,0 +1,85 @@
---
title: 'Admin einrichten'
description: 'tibi-admin lokal starten und mit dem Server verbinden: Installieren, Docker-Stack, URLs, Mock-Modus.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du richtest das **tibi-admin** (Verwaltungs-Backend, Release Nova) lokal ein und verbindest es mit dem Tibi-Server. Code-Repo ist ausschließlich `tibi-admin-nova` — alte `tibi-admin`-Repos ignorieren. Stack: **Svelte 5**, **esbuild**-Build, **Tailwind 4**, **Playwright**-Tests (siehe `package.json`).
## Voraussetzungen
| Braucht | Wofür |
|---|---|
| Node 20 + `yarn` | `yarn install`, esbuild-Build (`scripts/esbuild-wrapper.js`) |
| Docker + Compose | Lokaler Stack (`docker-compose-local.yml`, Profile `tibi` / `tibi-dev`) |
| Datei `.env` | Projekt-Name, URLs, Ports (Beispielwerte unten) |
```sh title="Versionen prüfen"
node --version # 20.x
yarn --version
docker compose version
```
## Starten
<Tabs>
<TabItem label="Docker-Stack (empfohlen)">
<Steps>
1. `cp .env.example .env` (falls vorhanden) und `PROJECT_NAME` setzen.
2. `make init` — legt `tmp/`-Verzeichnisse an.
3. `make docker-up` — startet Frontend-Build, `tibiserver`, Mongo, Maildev (`--profile tibi`).
4. Admin-URL aus `.env` öffnen (z. B. `https://<projekt>.code.testversion.online` bzw. lokal Port **3000**).
5. Auf der [Login-Seite](/admin/benutzer-rechte/) anmelden.
</Steps>
| Dienst | Port/Exposed | Hinweis |
|---|---|---|
| `yarnstart` (Admin-Frontend) | **3000** | Baut per `yarn install && yarn start…` mit `API_BASE` aus `.env` |
| `tibiserver` | **8080** | API unter `/api`, Admin ruft `/api/v1` |
| Mongo / Maildev | intern | Über Compose-Namen verdrahtet |
Nützliche Ziele: `make docker-ps`, `make docker-logs`, `make docker-down`, `make docker-restart-frontend`.
</TabItem>
<TabItem label="Nur Frontend (ohne Docker)">
<Steps>
1. `yarn install`
2. `API_BASE=https://<server>/api/v1 yarn start` (Dev) bzw. `yarn build` (statisches Bundle).
3. Browser auf die ausgegebene lokale URL zeigen.
</Steps>
`API_BASE` entscheidet, gegen welchen Server du arbeitest — Staging-URL steht in `.env` als `STAGING_TIBI_API_URL` (z. B. `https://dev-tibi-server.staging.testversion.online/api/v1`).
</TabItem>
</Tabs>
## Verbinden und Mock-Modus
| Modus | Erkennen | Verhalten |
|---|---|---|
| Echter Server | `API_BASE` zeigt auf laufenden tibi-server | Alle Daten live; Login per Benutzername/Passwort |
| Mock-Modus | Kein Server erreichbar / Mock-Flag (`isMockMode()`) | Lokale Demo-Daten, Demo-Projekt `t1`; Actions simulieren Fortschritt (`simulateMockProgress`) |
- Laufzeitwerte liegen in `frontend/assets/system.config.json` (KI-Endpunkte, TTS-Stimmen); Proxy-Ziele und Modelle in `config/ai-proxy.config.json` — siehe [KI-Funktionen](/admin/ki-funktionen/).
- Fehlerbilder bei falscher Verbindung: Login-Fehlerbox, Netzwerkfehler bei Listen/Actions — dann `API_BASE`, Server-Container (`make docker-ps`) und [Token-Köpfe](/reference/token-koepfe/) prüfen.
## Prüfen
```sh title="Build und Tests"
yarn validate # svelte-check + tsc
yarn build # esbuild-Bundle, muss fehlerfrei durchlaufen
yarn test:e2e # Playwright E2E
```
<Aside type="tip">Läuft `yarn build` grün und zeigt der Admin nach Login die Projektliste, ist das Setup fertig. Weiter geht es mit [Admin anpassen](/admin/customizing/).</Aside>
<!-- Screenshot-Platzhalter (T-SHOTS): frisch gestarteter Admin mit Projektliste nach Login. -->
Verifiziert am 2b091d899590b392491c59a54f50de43f1057280 2026-10-16 (tibi-admin-nova)
+11
View File
@@ -0,0 +1,11 @@
---
title: ADR-Vorlage
description: So halten wir Architektur-Entscheide fest.
---
## Vorlage
- **Datum:**
- **Kontext:**
- **Entscheidung:**
- **Folgen:**
+9
View File
@@ -0,0 +1,9 @@
---
title: Starlight als Doku-Basis
description: Warum Astro Starlight und statischer Build.
---
- **Datum:** 2026-10-16
- **Kontext:** Leere Doku, zwei Zielgruppen (Entwickler, Endkunden), Single Source in Quell-Repos.
- **Entscheidung:** Astro Starlight, Deutsch als Standard, statischer Build mit Pagefind-Suche, Theming an tibi-admin angelehnt.
- **Folgen:** Markdown-Quellen, Sidebar explizit in `astro.config.mjs`, Bilder unter `src/assets/` (max 200 KB, deutsche Alt-Texte).
@@ -0,0 +1,9 @@
---
title: Code schlägt Doku
description: Bestandsdoku gilt als Behauptung und wird am Code verifiziert.
---
- **Datum:** 2026-10-16
- **Kontext:** Bestandsdoku kann veraltet sein (Beispiel: Go-Version im README vs. `go.mod`, Collection-Config „Stand Februar 2026“ vs. aktuelle Schemas).
- **Entscheidung:** Quelle ist der Code (`internal/config`, Router, `internal/hook/context_*`, `tibi-types/schemas`, `frontend/src/lib/components`, `tests/e2e`, Test-Pläne). Übernommene Seiten tragen „verifiziert am <Commit> <Datum>“. Benennung: **tibi-server** und **tibi-admin** (Release Nova, Repo `tibi-admin-nova`), alte `tibi-admin`-Repos ignorieren.
- **Folgen:** Sync rendert nur, Handseiten (`server/uebersicht.md`, `admin/konzept.md`) ordnen. CI prüft Drift (`npm run sync && npm run build`).
@@ -0,0 +1,9 @@
---
title: Selbstläufer-Regel
description: Wie weiterentwickelnde Agenten die Doku automatisch mitpflegen.
---
- **Datum:** 2026-10-16
- **Kontext:** Tibi-Server, tibi-admin und Starter entwickeln sich weiter. Die Doku (57 Seiten, Stand 2026-10-16) soll nicht veralten.
- **Entscheidung:** Definition of Done für jede Änderung an API, Hooks, Collections, Permissions, Jobs, Realtime, LLM, MCP, Suche, Deploy oder Admin-UX: passende Doku-Seite aktualisieren + `Verifiziert am <Commit> <Datum>` erneuern + `npm run sync && npm run build` grün. Screenshots nur aus Mock-Projekt `corporate` (nie `t1`), Auftrag unter `plans/t-shots-arbeitsauftrag.md`. Vollständigkeits-Tabelle in `plans/doku-ausbau.md` bei neuen Fähigkeiten erweitern (1 Fähigkeit = 1 Dokustelle).
- **Folgen:** Diese Regel in die Agenten-Regeln von `tibi-server`, `tibi-admin-nova` und `tibi-svelte-starter` spiegeln (jeweils `AGENTS.md`-Ergänzung, noch offen). CI in tibi-docs prüft Drift (Docs neuer als Sync, Schema-Feld ohne Widget-Doku).
+73
View File
@@ -0,0 +1,73 @@
---
title: 'Anmelden'
description: 'Anmelden, Abmelden und Passwort im tibi-admin.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Du arbeitest im **tibi-admin**, dem Verwaltungs-Backend für deine Inhalte (Release **Nova**). Diese Seite beschreibt Anmeldung, Abmeldung und den Umgang mit Sitzungsende.
## Anmelden
<Steps>
1. Öffne die Anmeldeadresse deines tibi-admin im Browser.
2. Gib deinen **Benutzername** ein.
3. Gib dein **Passwort** ein.
4. Klicke auf **Anmelden**.
</Steps>
![Anmeldemaske von tibi-admin mit Feldern für Benutzername und Passwort](../../../assets/anwender/anmelden-login.webp)
<Aside type="tip">
Frage deine Betreuung nach der korrekten Adresse, falls du sie nicht kennst. Speichere sie anschließend als Lesezeichen.
</Aside>
## Wenn die Anmeldung fehlschlägt
Bleibst du auf der Anmeldeseite oder erscheint die Meldung „Ungültiger Benutzername oder Passwort", gehe wie folgt vor:
<Steps>
1. Prüfe Benutzername und Passwort Zeichen für Zeichen.
2. Deaktiviere die Feststelltaste und gib das Passwort erneut ein.
3. Klicke erneut auf **Anmelden**.
</Steps>
<Aside type="caution">
Führe höchstens drei Versuche durch und bitte danach deine Betreuung um ein neues Passwort, statt weiter zu raten.
</Aside>
## Abmelden und Profil
<Steps>
1. Klicke oben rechts auf deinen Namen oder dein Bild.
2. Wähle im Menü **Mein Profil**, um deine Daten einzusehen.
3. Wähle im Menü **Abmelden**, um die Sitzung zu beenden.
</Steps>
![Geöffnetes Benutzermenü oben rechts mit den Punkten Mein Profil und Abmelden](../../../assets/anwender/anmelden-abmelden.webp)
## Automatisches Sitzungsende
Bei längerer Inaktivität meldet dich tibi-admin automatisch ab. Es erscheint der Hinweis **Sitzung abgelaufen – bitte neu anmelden**.
<Steps>
1. Lies den Hinweis.
2. Öffne die Anmeldeseite erneut (der Hinweis führt dich dorthin zurück).
3. Melde dich wie oben mit Benutzername und Passwort an.
</Steps>
<Aside type="note">
Ungespeicherte Eingaben gehen beim automatischen Abmelden verloren. Speichere deine Arbeit daher regelmäßig. Details zum Speichern stehen unter [Einträge anlegen und bearbeiten](/anwender/eintrag-bearbeiten/).
</Aside>
Mehr Details zur Technik stehen unter [Mehr Details](/admin/konzept/).
Geprüft an 2b091d89 (tibi-admin-nova)
@@ -0,0 +1,68 @@
---
title: 'Benutzer und Teams'
description: 'Benutzer, Teams und Zuständigkeiten verstehen.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Du arbeitest im **tibi-admin**, dem Verwaltungs-Backend (Release **Nova**). Auf dieser Seite änderst du nichts — du liest ab, wer du bist und wer zuständig ist.
## Eigene Rolle prüfen
<Steps>
1. Klicke oben rechts auf deinen Namen.
2. Klicke auf **Mein Profil**.
3. Lies deinen Namen und deine E-Mail-Adresse ab.
4. Suche das Feld mit deiner Rolle.
</Steps>
<Aside type="note">
Die Rolle legt fest, was du darfst. Sie steht als Text bei deinem Namen, beispielsweise „Systemadministrator".
</Aside>
![Profilansicht mit Name, Rolle und persönlichen Daten](../../../assets/anwender/benutzer-profil.webp)
## Teams und Zuständigkeit
<Steps>
1. Öffne die **Benutzerverwaltung**, falls du sie im Menü siehst.
2. Suche dein Team in der Liste.
3. Klicke dein Team an.
4. Lies die Mitgliederliste.
</Steps>
<Aside type="note">
Ein Team gehört zu einer Organisation. So erkennst du, wer wofür zuständig ist. Der Menüpunkt **Benutzerverwaltung** erscheint nur mit Admin-Rolle — sein Fehlen ist daher kein Fehler.
</Aside>
![Teamansicht Content mit Mitgliederliste](../../../assets/anwender/benutzer-teams.webp)
## Anfrage stellen statt selbst ändern
Du legst keine Benutzer an und änderst keine Rechte — du stellst eine Anfrage.
<Steps>
1. Halte fest, was du benötigst (beispielsweise Zugang für eine Kollegin).
2. Notiere Team und gewünschte Rolle.
3. Sende diese Bitte an deine Teamleitung.
4. Warte auf die Bestätigung.
5. Prüfe danach, ob die Freischaltung vorliegt.
</Steps>
<Aside type="caution">
Gib keine Zugänge auf Zuruf frei und ändere keine Rechte eigenmächtig. Falsch vergebene Rechte lassen sich nur schwer zurückdrehen.
</Aside>
## Hinweis zur Stellvertretung
Meldet sich der Support mit deinem Konto an, um dir zu helfen (Stellvertretung über **Als dieser Benutzer einloggen**), geschieht das ausschließlich nach Absprache mit dir oder deiner Teamleitung. Sprich ihn an, wenn du unsicher bist. Du musst dafür nichts einrichten.
Was du über das Verwaltungs-Backend wissen musst, steht unter [Admin-Konzept](/admin/konzept/).
Geprüft an 2b091d89 (tibi-admin-nova)
@@ -0,0 +1,88 @@
---
title: 'Einträge anlegen und bearbeiten'
description: 'Neuen Eintrag erstellen, ausfüllen und speichern.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Ein Eintrag ist ein einzelner Inhalt, beispielsweise eine Nachricht. Du öffnest ihn aus einer Liste, wie unter [Lesen, suchen, filtern](/anwender/lesen-suchen-filtern/) beschrieben.
## Neuen Eintrag anlegen
<Steps>
1. Öffne die Sammlung, in die der Eintrag gehört.
2. Klicke oben auf **Neuer Eintrag**.
3. Warte, bis das leere Formular erscheint.
</Steps>
![Journal-Liste mit der Schaltfläche Neuer Eintrag oben rechts](../../../assets/anwender/eintrag-neu.webp)
<Aside type="tip">
Um einen bestehenden Eintrag zu ändern, klicke ihn in der Liste an statt auf Neu. So vermeidest du doppelte Inhalte.
</Aside>
<Aside type="caution">
Beim Duplizieren eines Eintrags werden direkte Datei-Felder in der Kopie geleert — lade die Dateien dort erneut hoch. Das ist beabsichtigt und kein Fehler.
</Aside>
## Formular ausfüllen
Links steht das Formular, rechts stehen Zusatzinformationen. Arbeite die Felder von oben nach unten ab.
<Steps>
1. Gib im ersten Textfeld den Titel ein.
2. Verfasse im großen Textfeld den Inhalt.
3. Stelle Zahlen über die Pfeile am Zahlenfeld ein.
4. Lege Schalter für Ja oder Nein um.
5. Wähle bei Datum das Datum im Kalender.
6. Wähle bei Auswahl einen Vorschlag aus der Liste.
7. Wähle bei Verweis auf einen anderen Inhalt den passenden Eintrag im Fenster **Durchsuchen**.
</Steps>
![Ausgefülltes Formular mit Titel, Teaser und Inhalt](../../../assets/anwender/eintrag-formular.webp)
<Aside type="note">
Ein Verweis verbindet zwei Inhalte, beispielsweise eine Nachricht mit einem Bild aus [Bilder und Dateien](/anwender/medien/). Du wählst ihn im Fenster **Durchsuchen** und übernimmst ihn mit **Auswählen**.
</Aside>
## Speichern und prüfen
<Steps>
1. Klicke oben auf **Speichern**.
2. Warte auf die Bestätigung **Eintrag gespeichert**.
3. Lade die Seite einmal neu und prüfe deine Änderung.
</Steps>
![Gespeicherter Eintrag mit der Bestätigung Gespeichert oben rechts](../../../assets/anwender/eintrag-gespeichert.webp)
<Aside type="caution">
Verlässt du die Seite mit ungespeicherten Änderungen, erscheint die Rückfrage **Änderungen verwerfen?** Brich ab und speichere zuerst, sonst gehen die Eingaben verloren.
</Aside>
## Wenn das Speichern blockiert ist
Rot markierte Felder bedeuten: Hier fehlt eine Angabe oder sie ist ungültig.
<Steps>
1. Lies den Hinweis an der Speichern-Schaltfläche (Anzahl der Fehler).
2. Klicke auf das erste rot markierte Feld.
3. Fülle Pflichtfelder aus, sie sind mit Stern markiert.
4. Beachte bei besonderen Feldern das Format, beispielsweise Datum oder Zahl.
5. Klicke erneut auf **Speichern**.
</Steps>
<Aside type="note">
Schlägt ein Verweis fehl, ist häufig der Filter im Fenster **Durchsuchen** zu eng. Lösche dort die Suche und wähle erneut. Bleibt das Problem bestehen, bitte deine Betreuung um die erforderlichen Rechte für den verknüpften Inhalt.
</Aside>
Mehr Details zur Technik stehen unter [Mehr Details](/admin/konzept/).
Geprüft an 2b091d89 (tibi-admin-nova)
+65
View File
@@ -0,0 +1,65 @@
---
title: 'Exportieren'
description: 'Listen als CSV, XLSX oder JSON exportieren.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Du lädst eine Liste aus dem **tibi-admin**, dem Verwaltungs-Backend (Release **Nova**), als Datei herunter. Exportiert wird genau das, was du in der Liste siehst — Filter gelten auch für die Datei.
## Export öffnen
<Steps>
1. Öffne die Liste, die du benötigst.
2. Stelle Filter und Suche wie erforderlich ein.
3. Klicke auf **Export** über der Liste.
4. Warte, bis sich der Dialog **Export** öffnet.
5. Prüfe, ob die Auswahl zu deinem Filter passt.
</Steps>
![Export-Dialog mit Formatwahl und Spaltenauswahl](../../../assets/anwender/export-dialog.webp)
## Format wählen und laden
<Steps>
1. Klicke auf ein Format (CSV, XLSX oder JSON).
2. Wähle die benötigten Felder (**Nur sichtbare** oder **Alle auswählen**).
3. Lege bei Arrays die Array-Behandlung fest (**JSON** oder **Dynamische Spalten**).
4. Klicke auf **Exportieren**.
5. Warte, bis der Fortschritt abgeschlossen ist.
6. Öffne die Datei auf deinem Rechner.
</Steps>
<Aside type="note">
CSV eignet sich für einfache Tabellen, XLSX für aufbereitete Tabellen, JSON für den verlustfreien Austausch mit anderen Programmen (Array-Behandlung **JSON**).
</Aside>
<Aside type="caution">
Erscheint die Warnung **Viele Spalten** (Feld mit unterschiedlich langen Aufzählungen), wechsle auf **JSON** (**Auf JSON umschalten**) oder wähle weniger Spalten. Bei Abbruch bleibt der Dialog offen; es geht nichts verloren.
</Aside>
## Datei prüfen
<Steps>
1. Öffne die heruntergeladene Datei.
2. Prüfe die ersten Zeilen.
3. Kontrolliere grob, ob die Menge zu Filter und Suche passt.
4. Speichere die Datei an einem geeigneten Ort.
5. Schließe den Dialog im tibi-admin.
</Steps>
![Bestätigung 5 Einträge exportiert nach abgeschlossenem Export](../../../assets/anwender/export-fertig.webp)
<Aside type="note">
Bei sehr großen Listen warnt dich tibi-admin vorab (**Große Sammlung**). Wiederhole den Export in diesem Fall in kleineren Teilen. Enthält die Datei falsche Einträge, war der Filter enger als angenommen — schließe den Dialog, prüfe Filter und Suche und starte den Export erneut.
</Aside>
Wie du Listen filterst, steht unter [Lesen, Suchen, Filtern](/anwender/lesen-suchen-filtern/).
Geprüft an 2b091d89 (tibi-admin-nova)
+91
View File
@@ -0,0 +1,91 @@
---
title: 'Hilfe, wenn es klemmt'
description: 'Häufige Fragen und was du dann tust.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Diese Seite fasst die häufigsten Probleme im **tibi-admin** (Release **Nova**) und ihre Lösung zusammen. Gehe die Punkte der Reihe nach durch.
## Anmeldung schlägt fehl
<Steps>
1. Prüfe die Adresse im Browser (Staging und Live nicht verwechseln).
2. Gib Benutzername und Passwort erneut ein.
3. Klicke auf **Anmelden**.
4. Warte die Rückmeldung ab.
</Steps>
<Aside type="note">
„Ungültiger Benutzername oder Passwort" bedeutet: Name oder Passwort stimmen nicht. Das System nennt aus Sicherheitsgründen nicht, welches von beiden. Details stehen unter [Anmelden](/anwender/anmelden/).
</Aside>
<Aside type="caution">
Lautet die Meldung „Keine Berechtigung für diese Aktion", fehlt dir das Recht für dieses Projekt. Bitte deine Teamleitung um Freischaltung.
</Aside>
## Eintrag wird nicht gefunden
<Steps>
1. Drücke „/" und gib einen kurzen Begriff in der Schnellsuche ein.
2. Wechsle in die passende Sammlung in der linken Liste.
3. Setze alle Filter zurück (**Alle zurücksetzen**).
4. Blättere eine Seite weiter.
</Steps>
<Aside type="note">
Häufige Ursachen: falsche Sammlung, aktiver Filter oder Tippfehler. Suchtipps stehen unter [Lesen, Suchen, Filtern](/anwender/lesen-suchen-filtern/).
</Aside>
## Speichern ist blockiert
<Steps>
1. Lies die rote Meldung am Feld.
2. Fülle das markierte Pflichtfeld aus.
3. Kürze zu lange Texte.
4. Klicke erneut auf **Speichern**.
</Steps>
<Aside type="note">
„Dieses Feld ist erforderlich" bedeutet: Ein Feld mit Stern darf nicht leer bleiben. „Eintrag nicht gefunden" bedeutet: Der Eintrag wurde gelöscht oder du bist in der falschen Sammlung — lege ihn nicht doppelt an, sondern bitte deine Teamleitung um Prüfung.
</Aside>
## Bild fehlt
<Steps>
1. Öffne den Eintrag mit dem Bild.
2. Klicke das Bildfeld an.
3. Prüfe, ob dort ein Bild gewählt ist.
4. Wähle das Bild erneut aus der Mediathek.
5. Klicke auf **Speichern**.
</Steps>
<Aside type="note">
Bleibt das Bild nach dem Speichern leer, bitte deine Betreuung um Prüfung der Freigabe. Mehr dazu unter [Medien](/anwender/medien/).
</Aside>
## An wen du dich wendest
<Steps>
1. Lies die Meldung auf dem Bildschirm wörtlich ab.
2. Halte fest, was du unmittelbar zuvor angeklickt hast.
3. Wende dich bei Inhalten (Texte, Bilder, Seiten) an deine Teamleitung.
4. Wende dich bei Technik (Fehler, Tempo, Ausfall) an den Betreiber.
5. Wende dich bei Zugängen (Passwort, Rolle, Team) an deine Teamleitung.
</Steps>
<Aside type="note">
Ist niemand erreichbar, fertige einen Screenshot der Meldung an, notiere Uhrzeit und Projekt und sende alles per Nachricht weiter.
</Aside>
Geprüft an 2b091d89 (tibi-admin-nova)
+105
View File
@@ -0,0 +1,105 @@
---
title: 'KI nutzen'
description: 'Nova im Admin: chatten, zusammenfassen, diktieren und vorlesen lassen.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Die KI **Nova** im **tibi-admin** (Release **Nova**) hilft dir beim Redigieren: Sie beantwortet Fragen zur geöffneten Ansicht, fasst lange Gespräche zusammen und versteht gesprochene Eingaben. So nutzt du sie.
## Chat öffnen und fragen
<Steps>
1. Klicke auf **Nova öffnen**.
2. Schreibe deine Frage in das Feld **Nachricht eingeben…** und klicke auf **Senden**.
3. Lies die Antwort — Nova begrüßt dich mit „Hallo! Ich bin Nova. Wie kann ich dir helfen?".
4. Stelle Rückfragen oder beginne über **Neuer Chat** von vorn.
</Steps>
<Aside type="note">
Nova sieht die gerade geöffnete Ansicht (Projekt, Sammlung, Eintrag). Wechsle daher zuerst dorthin, worum es in deiner Frage geht.
</Aside>
![Geöffneter Nova-Chat mit Begrüßung, Frage und Antwort im Corporate-Projekt](../../../assets/anwender/ki-chat.webp)
## Was Nova für dich erledigt
<Steps>
1. Bitte um **Suchen**: Nova durchsucht die Einträge der geöffneten Sammlung nach deinem Begriff.
2. Bitte um **Filter setzen**: Nova grenzt die Liste ein, zum Beispiel auf einen Zeitraum oder einen Status.
3. Bitte um **Formular ausfüllen**: Nova trägt Werte in die Felder des geöffneten Eintrags ein — du prüfst und speicherst selbst.
</Steps>
<Aside type="caution">
Bei heiklen Schritten fragt Nova vorher um Erlaubnis. Lies jede Änderung im Formular und klicke erst dann auf **Speichern**.
</Aside>
## Lange Gespräche zusammenfassen
<Steps>
1. Frage einfach weiter — um volle Verläufe kümmert sich Nova selbst.
2. Währenddessen meldet Nova **Kontext wird zusammengefasst…**.
3. Danach steht **Kontext zusammengefasst** bzw. **Zusammenfassung aktiv** mit der Angabe, wie viele Nachrichten verdichtet wurden.
4. Den Füllstand prüfst du über **Token-Status** unter **Kontext-Auslastung**.
</Steps>
<Aside type="note">
Geht dir der Zusammenhang verloren, beginne über **Neuer Chat** von vorn und stelle die letzte Frage erneut.
</Aside>
![Token-Status mit Kontext-Auslastung und Zählern der Unterhaltung](../../../assets/anwender/ki-zusammenfassen.webp)
## Sprechen statt tippen
<Steps>
1. Klicke auf **Spracheingabe** und sprich deine Frage aus.
2. Der Status zeigt **Hört auf diesem Gerät** oder **Nimmt für KI-Transkription auf**.
3. Der erkannte Text erscheint im Eingabefeld (**Wird transkribiert…** währenddessen).
4. Beende mit **Aufnahme stoppen** oder verwerfe mit **Abbrechen**.
</Steps>
<Steps>
1. Lasse dir eine Antwort mit **Vorlesen** vortragen (**Audio wird geladen…** während des Ladens).
2. Stoppe mit **Wiedergabe stoppen**.
3. Schalte die Sprachausgabe über **Sprachausgabe** ein und über **Sprachausgabe aus** wieder aus; das Tempo stellst du unter **Geschwindigkeit** ein (**Zurücksetzen** stellt den Standard wieder her).
</Steps>
<Aside type="note">
Für die Spracheingabe benötigt der Browser Zugriff auf dein Mikrofon. Verweigert er ihn, erlaube ihn in den Browser-Einstellungen.
</Aside>
<Aside type="note">
Deine Unterhaltung bleibt lokal in diesem Browser gespeichert. Die Sprachaufnahme zur Verschriftlichung geht zur Verarbeitung an den Server — sprich keine vertraulichen Daten ein, die dort nichts zu suchen haben.
</Aside>
![Spracheingabe mit Status Hört auf diesem Gerät, diktiertem Text im Eingabefeld und Vorlesen-Knopf an der Antwort](../../../assets/anwender/ki-stimme.webp)
## Grenzen
<Steps>
1. Erscheint „Rate-Limit erreicht. Bitte warte einen Moment und versuche es erneut.", warte kurz und versuche es erneut.
2. Meldet Nova „KI-Dienst ist nicht erreichbar. Bitte versuche es später erneut." oder „Der KI-Dienst ist vorübergehend nicht erreichbar. Bitte versuche es später erneut.", prüfe deine Verbindung und versuche es später erneut.
3. Steht dort „KI-Dienst nicht konfiguriert. Bitte kontaktiere den Administrator.", wende dich an den Betreiber.
</Steps>
<Aside type="caution">
Nova kennt nur, was im Admin geöffnet ist, und kann irren. Prüfe wichtige Angaben in der Liste oder im Eintrag, bevor du speicherst oder weiterleitest. Die Nutzung ist mengenmäßig begrenzt — stelle Fragen gezielt, statt denselben Auftrag mehrfach zu wiederholen.
</Aside>
Mehr Details zur Technik stehen unter [Mehr Details](/admin/konzept/). Kommt die KI nicht weiter, hilft [Hilfe, wenn es klemmt](/anwender/hilfe/).
Geprüft an 2b091d89 (tibi-admin-nova)
Screenshots: Mock-Projekt Corporate Website; KI-Antworten und Statusanzeigen simuliert, kein echter Dienst. Die Zusammenfassung lief ohne sichtbares Banner durch — der Shot zeigt den zugehörigen Token-Status.
@@ -0,0 +1,103 @@
---
title: 'Lesen, suchen, filtern'
description: 'Listen lesen, suchen, sortieren und filtern.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Du hast eine Sammlung geöffnet und siehst eine Liste mit Einträgen. Diese Seite beschreibt Lesen, Suchen, Filtern, Sortieren und Blättern. Wie du dorthin gelangst, steht unter [Sich zurechtfinden](/anwender/orientierung/).
## Liste lesen
Oben steht der Name der Sammlung, darunter die Einträge als **Tabelle** oder **Karten**.
<Steps>
1. Lies die erste Zeile von links nach rechts als Beispiel.
2. Beachte oben die Spaltenüberschriften, sie benennen die Inhalte.
3. Wechsle oben rechts die Ansicht, um zwischen **Tabelle** und **Karten** umzuschalten.
</Steps>
![Journal-Liste als Tabelle mit Suchfeld und Einträgen](../../../assets/anwender/lesen-liste.webp)
<Aside type="tip">
Deine gewählte Ansicht bleibt erhalten. Nach einem Neuladen siehst du dieselbe Ansicht erneut.
</Aside>
![Journal-Einträge in der Kartenansicht](../../../assets/anwender/lesen-karten.webp)
## Suchen
<Steps>
1. Klicke oben in das Suchfeld **Einträge durchsuchen…**.
2. Gib einen Begriff ein und warte einen Moment.
3. Die Liste zeigt nur noch die Treffer.
</Steps>
<Aside type="note">
Am Suchfeld befindet sich ggf. ein Schalter für den Suchmodus. Belasse ihn unverändert, sofern deine Betreuung keine andere Vorgabe macht.
</Aside>
Liefert die Suche keine Treffer, gehe wie folgt vor:
<Steps>
1. Kürze den Suchbegriff und suche erneut.
2. Prüfe die Schreibweise Zeichen für Zeichen.
3. Leere das Suchfeld vollständig, um wieder alle Einträge zu sehen.
</Steps>
![Suche nach Operations mit einem Treffer in der Journal-Liste](../../../assets/anwender/lesen-suche.webp)
## Filtern und sortieren
Filtern bedeutet: Du zeigst nur Einträge mit einer bestimmten Eigenschaft, beispielsweise einer Sprache. Sortieren bedeutet: Du änderst die Reihenfolge.
<Steps>
1. Klicke oben auf die Schaltfläche **Filter**.
2. Wähle eine Eigenschaft aus der Liste.
3. Wähle einen Wert und wende den Filter an.
4. Klicke auf eine Spaltenüberschrift, um auf- oder absteigend zu sortieren.
</Steps>
<Aside type="caution">
Bleibt die Liste nach dem Filtern leer, ist ein Filter zu eng. Öffne das Filterfenster und wähle **Alle zurücksetzen**, danach erscheinen alle Einträge wieder.
</Aside>
![Geöffnetes Filterfenster neben der sortierten Journal-Liste](../../../assets/anwender/lesen-filter.webp)
## Blättern
Bei vielen Einträgen erscheinen unten Seitenzahlen.
<Steps>
1. Scrolle nach unten zur Fußzeile.
2. Klicke auf die nächste Seitenzahl.
3. Lies oben die Angabe, welche Einträge gerade angezeigt werden.
</Steps>
## Eintrag öffnen
<Steps>
1. Klicke auf die Zeile oder Karte, die dich interessiert.
2. Warte, bis sich der Eintrag öffnet.
3. Nutze den Navigationspfad oben, um zur Liste zurückzukehren.
</Steps>
<Aside type="tip">
Suche und Seitenposition bleiben erhalten, wenn du zurückkehrst. Weiter geht es mit [Einträge anlegen und bearbeiten](/anwender/eintrag-bearbeiten/).
</Aside>
Mehr Details zur Technik stehen unter [Mehr Details](/admin/konzept/).
Geprüft an 2b091d89 (tibi-admin-nova)
+95
View File
@@ -0,0 +1,95 @@
---
title: 'Bilder und Dateien'
description: 'Dateien hochladen und Medien verwalten.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Bilder und Dateien liegen in einer eigenen Sammlung, meist Mediathek genannt. Von dort bindest du sie in Einträge ein, wie unter [Einträge anlegen und bearbeiten](/anwender/eintrag-bearbeiten/) beschrieben.
## Übersicht öffnen
<Steps>
1. Öffne die Sammlung für Bilder und Dateien.
2. Betrachte die Kacheln mit Vorschaubild und Titel.
3. Klicke auf eine Kachel, um die **Medienvorschau** zu öffnen.
</Steps>
![Mediathek mit Kachel-Raster und Vorschaubildern](../../../assets/anwender/medien-raster.webp)
<Aside type="tip">
In der **Medienvorschau** blätterst du mit **Vorheriges** und **Nächstes**. Schließe sie mit **Schließen**, um zum Raster zurückzukehren.
</Aside>
## Datei hochladen
<Steps>
1. Ziehe eine Datei mit der Maus auf das Raster.
2. Klicke alternativ auf die Kachel **Dateien hinzufügen** am Ende des Rasters und wähle Dateien aus.
3. Beobachte den Fortschritt pro Datei (Anzeige „x von y hochgeladen").
4. Warte, bis die neue Kachel im Raster erscheint.
</Steps>
<Aside type="note">
Titel und Beschreibung werden aus dem Dateinamen vorbefüllt. Prüfe sie und korrigiere bei Bedarf.
</Aside>
![Hochladen einer Datei mit Fortschrittsanzeige in der Mediathek](../../../assets/anwender/medien-upload.webp)
Wird ein Upload abgelehnt, gehe wie folgt vor:
<Steps>
1. Prüfe, ob die Datei die zulässige Größe überschreitet („Datei ist zu groß"), und wähle eine kleinere Datei.
2. Prüfe das Format („Dateityp ist nicht erlaubt") und verwende beispielsweise JPG.
3. Wiederhole den Versuch mit einer Datei pro Durchgang.
</Steps>
<Aside type="caution">
Besteht die Ablehnung fort, bitte deine Betreuung um Prüfung. Nenne Dateiname, Größe und Format.
</Aside>
## Bild anpassen
<Steps>
1. Klicke auf der Kachel auf **Bild bearbeiten**.
2. Wähle oben ein Werkzeug: **Zuschneiden**, **Anpassen** oder **Skalieren**.
3. Ziehe den Rahmen mit der Maus auf den gewünschten Bereich.
4. Lege bei Bedarf **Fokuspunkt** oder **Leitfarben** fest.
5. Klicke auf **OK**, um die Änderung zu übernehmen.
</Steps>
<Aside type="caution">
Ohne **OK** geht die Anpassung verloren. Klicke zuerst auf **OK** und speichere danach den Eintrag wie unter [Einträge anlegen und bearbeiten](/anwender/eintrag-bearbeiten/) beschrieben.
</Aside>
![Bildeditor mit Werkzeugen für Zuschnitt, Anpassen und Skalieren](../../../assets/anwender/medien-editor.webp)
## Direkt bearbeiten und KI-Beschreibung
In der Upload-Warteschlange („{done} von {total} hochgeladen") bearbeitest du Einträge vor dem Hochladen: **Bearbeiten & hochladen** öffnet den Eintrag im Dialog (**Medieneintrag bearbeiten**), **Schnell-Upload** lädt ohne Bearbeitung. Große Bilder verkleinert tibi-admin automatisch („Bild auf Breite×Höhe verkleinert").
Fehlt die Bildbeschreibung, erstellt die KI sie selbst („KI beschreibt das Bild…" → „KI-Beschreibung bereit"). Bereits ausgefüllte Beschreibungen bleiben stehen. Bei Bedarf brichst du ab (**KI-Beschreibung abbrechen**) oder stößt sie erneut an.
## Bild in einen Eintrag einbinden
<Steps>
1. Öffne den Eintrag, der das Bild benötigt.
2. Klicke im Bildfeld auf **Bild hochladen** bzw. die Auswahlfläche.
3. Suche im Auswahlfenster nach dem Bild.
4. Klicke auf das Bild und übernimm es.
5. Speichere den Eintrag oben mit **Speichern**.
</Steps>
Mehr Details zur Technik stehen unter [Mehr Details](/admin/konzept/).
Geprüft an 2b091d89 (tibi-admin-nova)
+88
View File
@@ -0,0 +1,88 @@
---
title: 'Sich zurechtfinden'
description: 'Start, Navigation und Übersicht im tibi-admin.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Nach dem [Anmelden](/anwender/anmelden/) landest du im **tibi-admin**, dem Verwaltungs-Backend (Release **Nova**). Diese Seite beschreibt Projektübersicht, Navigation und Schnellsuche.
## Projektübersicht
<Steps>
1. Prüfe oben links Logo und Projektnamen.
2. Orientiere dich am Navigationspfad in der Mitte (Projekt, Sammlung, Eintrag).
3. Öffne links das Menü, um alle Bereiche zu sehen.
</Steps>
![Projektübersicht der Corporate Website mit Kopf, Brotkrumen und Sammlungen](../../../assets/anwender/orientierung-start.webp)
<Aside type="tip">
Der Navigationspfad ist dein Rückweg. Ein Klick auf eine frühere Station führt direkt dorthin zurück.
</Aside>
## Projekt wechseln
Ein Projekt ist beispielsweise eine Website oder ein Shop.
<Steps>
1. Klicke oben links auf den Projektnamen.
2. Wähle in der Liste das gewünschte Projekt.
3. Warte, bis dessen Sammlungen angezeigt werden.
</Steps>
<Aside type="caution">
Erscheint ein Projekt nicht in der Liste, fehlt dir dafür die Berechtigung. Bitte deine Betreuung um Zugang.
</Aside>
![Projektliste mit allen Projekten, darunter die Corporate Website](../../../assets/anwender/orientierung-projektwechsel.webp)
## Sammlung öffnen
Eine Sammlung ist eine Liste gleichartiger Inhalte, beispielsweise alle Seiten oder alle Nachrichten.
<Steps>
1. Klicke im Menü auf die gewünschte Sammlung.
2. Warte, bis die Eintragsliste erscheint.
3. Klicke auf einen Eintrag, um ihn zu lesen.
</Steps>
Wie du in Listen suchst und filterst, steht unter [Lesen, suchen, filtern](/anwender/lesen-suchen-filtern/).
## Schnellsuche
Die Schnellsuche springt zu Zielen wie Sammlungen und Projekten. Inhalte suchst du in der jeweiligen Liste.
<Steps>
1. Drücke die Taste „/" (Schrägstrich).
2. Gib einen Suchbegriff ein, beispielsweise einen Titel.
3. Wähle einen Vorschlag, um dorthin zu springen.
</Steps>
<Aside type="note">
Öffnet sich das Fenster nicht, klicke einmal in die Seite und drücke „/" erneut. Im Text-Editor fügt Strg + K einen Link ein und öffnet keine Suche — die Schnellsuche reagiert ausschließlich auf „/".
</Aside>
![Geöffnete Schnellsuche mit Treffer Corporate Website öffnen](../../../assets/anwender/orientierung-schnellsuche.webp)
## Ausgangspunkt wiederfinden
<Steps>
1. Klicke im Navigationspfad auf die erste Station.
2. Du landest auf der Projektübersicht und beginnst von dort erneut.
3. Alternativ nutzt du die Zurück-Taste des Browsers — die vorherige Ansicht wird wiederhergestellt.
</Steps>
Mehr Details zur Technik stehen unter [Mehr Details](/admin/konzept/).
Geprüft an 2b091d89 (tibi-admin-nova)
+129
View File
@@ -0,0 +1,129 @@
---
title: 'Seiten bauen'
description: 'Einfache Seiten mit dem Pagebuilder zusammenstellen.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Du baust eine Seite im **tibi-admin**, dem Verwaltungs-Backend (Release **Nova**). Die Seite erscheint erst auf der Projekt-Website, wenn du sie per **Live**-Schalter freigibst.
## Block hinzufügen
<Steps>
1. Klicke in der linken Liste auf deine Sammlung für Seiten.
2. Klicke den Eintrag an, den du bearbeiten willst.
3. Klicke im Seitenfeld auf **Block hinzufügen**.
4. Wähle einen Blocktyp, zum Beispiel Hero, Kennzahlen, Story-Sektion oder FAQ.
5. Der Block liegt sofort in der Seite — ein Klick genügt, es gibt kein Übernehmen.
</Steps>
<Aside type="note">
Ein Block ist ein Baustein. Eine Seite besteht aus mehreren Bausteinen untereinander.
</Aside>
<Aside type="caution">
Fehlt die Schaltfläche **Block hinzufügen**, verfügst du über Lese-Recht. Bitte deine Teamleitung um Schreib-Recht. Mehr dazu unter [Benutzer und Teams](/anwender/benutzer-teams/).
</Aside>
![Pagebuilder mit geöffneter Blockauswahl für Corporate-Blöcke](../../../assets/anwender/seiten-pagebuilder.webp)
## Text und Bild füllen
<Steps>
1. Klicke in das Textfeld des neuen Blocks.
2. Schreibe deinen Text direkt hinein.
3. Klicke beim Bild-Block auf **Bild hochladen**.
4. Wähle ein Bild aus der Mediathek.
5. Klicke auf **Speichern** oben rechts.
</Steps>
<Aside type="note">
Die kleine Vorschau benötigt nach der Bildauswahl einen Moment. Ein Platzhalterbild ist in dieser Phase normal; nach dem Speichern erscheint das gewählte Bild.
</Aside>
Mehr zu Bildern findest du unter [Medien](/anwender/medien/).
![Seite mit ausgefüllten Corporate-Blöcken im Pagebuilder](../../../assets/anwender/seiten-bloecke.webp)
## Vorschau prüfen
Jeder Block zeigt seine Vorschau direkt an — du siehst sofort, wie er wirkt. Es gibt keinen separaten Vorschau-Knopf.
<Steps>
1. Betrachte den neuen Block direkt in der Seite.
2. Fülle ihn aus und beobachte die Vorschau.
3. Klicke den Block an, wenn du etwas ändern willst.
4. Klicke auf **Speichern**, wenn alles stimmt.
</Steps>
<Aside type="tip">
Die Vorschau ist nur für dich sichtbar. Besucher sehen sie nicht.
</Aside>
## Block bearbeiten
Ein Klick auf einen Block öffnet den Overlay-Editor: Formular links, Vorschau rechts. Verfügbare Blocktypen: Hero, Kennzahlen, Leistungsgrid, Story-Sektion, Stimmen, FAQ, Abschluss-CTA.
![Overlay-Editor des Hero-Blocks der Kontakt-Seite mit Formular links und Vorschau rechts, oben Modus-Umschalter (Nur Formular, Formular und Vorschau, Nur Vorschau) und OK-Schaltfläche](../../../assets/anwender/seiten-overlay.webp)
<Steps>
1. Klicke den Block in der Übersicht an — der Editor öffnet sich als Overlay.
2. Fülle links das Formular aus (Text, Bild, Link). Die Vorschau rechts aktualisiert sich sofort.
3. Nutze bei Bedarf den Modus-Umschalter in der oberen Leiste: **Nur Formular**, **Formular und Vorschau** oder **Nur Vorschau**. Auf schmalen Bildschirmen (unter 768 px) steht nur **Nur Formular** zur Verfügung.
4. Klicke auf **OK** oder drücke ESC, um den Editor zu schließen. Ein Klick genügt — es gibt keinen Übernehmen-Button.
5. Klicke auf **Speichern** oben rechts, um die Seite zu sichern.
</Steps>
<Aside type="note">
Das Block-Label mit Farbpunkt zeigt Typ und Nummer des Blocks, beispielsweise Hero #1. Zeigt ein Block statt einer Grafik nur Textwerte, besitzt dieser Blocktyp keine Vorschau-Grafik — die Seite funktioniert dennoch.
</Aside>
## Responsiv prüfen
Unter der Vorschau steuert der Viewport-Umschalter die simulierte Seitenbreite. So prüfst du, wie der Block auf kleineren Geräten wirkt.
![Overlay-Editor mit Tablet-Vorschau (768 px) des Hero-Blocks, Viewport-Umschalter unter der Vorschau](../../../assets/anwender/seiten-viewport-tablet.webp)
![Overlay-Editor mit Mobil-Vorschau (375 px) des Hero-Blocks, Viewport-Umschalter unter der Vorschau](../../../assets/anwender/seiten-viewport-mobil.webp)
<Steps>
1. Öffne einen Block im Overlay-Editor.
2. Klicke unter der Vorschau auf eines der drei Geräte-Symbole (Bildschirm, Tablet, Telefon — die Namen Desktop, Tablet und Mobil mit Breite erscheinen beim Darüberfahren; die aktive Breite steht als px-Zahl in der Leiste).
3. Ziehe bei Bedarf den Slider für eine freie Breite (320–1920 px).
4. Prüfe, ob Text lesbar bleibt und Bilder nicht abgeschnitten sind.
5. Schließe mit **OK** und klicke auf **Speichern**, wenn alles stimmt.
</Steps>
<Aside type="tip">
Die Vorschau ist inline — du benötigst keinen separaten Vorschau-Knopf und keine Zurück-Taste.
</Aside>
## Seite sichtbar schalten
<Steps>
1. Klicke den Eintrag deiner Seite an.
2. Suche den Schalter **Live** (Name je nach Projekt — er steuert die Sichtbarkeit für Besucher).
3. Schalte ihn auf An.
4. Klicke auf **Speichern**.
5. Rufe die Website in einem neuen Fenster auf und prüfe die Seite.
</Steps>
<Aside type="caution">
Erst mit **Live** wird die Seite für Besucher sichtbar. Ohne diesen Schalter bleibt sie ein Entwurf.
</Aside>
Weiter geht es mit [Einträge anlegen und bearbeiten](/anwender/eintrag-bearbeiten/). Dort lernst du alle Felder kennen.
Geprüft an 2b091d89 (tibi-admin-nova)
+67
View File
@@ -0,0 +1,67 @@
---
title: 'Sichern und wiederherstellen'
description: 'Snapshots ansehen und Wiederherstellung beauftragen.'
---
import { Aside, Steps } from '@astrojs/starlight/components';
Der **tibi-admin** (Verwaltungs-Backend, Release **Nova**) kennt zwei Sicherungen: Snapshots (eingefrorene Stände für den Alltag) und Backups (Dateien für Umzug und Notfall). Du spielst nichts selbst zurück — du prüfst den Stand und forderst bei Bedarf eine Wiederherstellung an.
## Gesicherten Stand einsehen
<Steps>
1. Klicke auf dein Projekt in der Übersicht.
2. Öffne den Bereich **Admin**.
3. Klicke auf **Snapshots**.
4. Lies die vorhandene Liste.
</Steps>
<Aside type="note">
Ein Snapshot liegt auf dem Server; ein Backup lädst du als Datei herunter. Siehst du den Menüpunkt nicht, fehlt dir das Recht dafür — bitte deine Teamleitung oder den Betreiber um Einsicht.
</Aside>
![Snapshot-Übersicht mit Einträgen des Corporate-Projekts](../../../assets/anwender/sichern-uebersicht.webp)
## Snapshot prüfen
Die Ansicht besitzt drei Reiter: **Konfigurationen**, **Snapshots** und **Statistik**.
<Steps>
1. Klicke auf den Reiter **Snapshots**.
2. Klicke eine Zeile an, um die **Snapshot-Details** zu öffnen.
3. Lies Datum, Umfang und Größe.
4. Klicke auf den Reiter **Statistik**.
5. Prüfe dort die Belegung.
</Steps>
<Aside type="note">
Der Reiter **Konfigurationen** zeigt die Zeitpläne — sofern dein Projekt welche eingerichtet hat. Frage im Zweifel den Betreiber, ob und wann Sicherungen laufen. Ist die Liste leer, existiert noch kein Snapshot.
</Aside>
![Geöffnete Snapshot-Details mit Aufschlüsselung nach Sammlungen](../../../assets/anwender/sichern-details.webp)
![Statistik-Reiter mit Belegung der Snapshots](../../../assets/anwender/sichern-statistik.webp)
## Wiederherstellung anfordern
Du spielst nichts selbst zurück. Eine Wiederherstellung (**Wiederherstellung starten**) überschreibt den aktuellen Stand und wird daher immer mit dem Betreiber abgestimmt.
<Steps>
1. Halte fest, welchen Stand du benötigst (Datum und Uhrzeit).
2. Notiere, welche Sammlung betroffen ist.
3. Sende diese Bitte an den Betreiber.
4. Warte auf die Bestätigung vom Betreiber.
5. Prüfe danach die betroffene Seite.
</Steps>
<Aside type="caution">
Stoße keine Wiederherstellung ohne Auftrag an.
</Aside>
Geprüft an 2b091d89 (tibi-admin-nova)
+4 -2
View File
@@ -13,5 +13,7 @@ description: Die wichtigsten Tibi-Begriffe in einem Glossar.
| **PAT** | Personal Access Token für API-Zugriff ohne Login. |
| **SSE-Kanal** | Server-Sent-Events-Kanal für Echtzeit (pub/sub mit Replay und TTL). |
| **SSR-Hook** | Server-Side-Rendering für Kunden-SPAs (`api/hooks/ssr/`), HTML-Cache in der `ssr`-Collection. |
| **Nova** | Das Svelte-Admin-Frontend (`tibi-admin-nova`). |
| **Mock-Modus** | Nova läuft lokal gegen gemockte Daten statt gegen den echten Server. |
| **Tibi-Admin** | Das Verwaltungs-Backend: Bedienoberfläche zum Tibi-Server (aktuelles Release Nova, Repo `tibi-admin-nova`). Hier pflegst du Inhalte, Medien, Benutzer und Exporte. |
| **Tibi-Projekt** | Eigenständige Website auf Tibi-Basis aus `tibi-svelte-starter` — normale kleine Websites, Shops, Intranet-Anwendungen usw. Läuft getrennt vom Backend, bezieht Inhalte per API/SSR. |
| **Nova** | Release-Name des aktuellen tibi-admin (Repo `tibi-admin-nova`). |
| **Mock-Modus** | tibi-admin läuft lokal gegen gemockte Daten statt gegen den echten Server. |
+115
View File
@@ -0,0 +1,115 @@
---
title: Neues Projekt aufsetzen
description: Vom geklonten Starter zum erreichbaren Projekt — inklusive Registrierung und Abnahme.
---
import { FileTree } from '@astrojs/starlight/components';
Anleitung für **Projekt-Entwickler**: du klonst `tibi-svelte-starter` und baust daraus eine eigenständige Website auf Tibi-Basis — normale kleine Websites, Shops, Intranet-Anwendungen usw. Das Verwaltungs-Backend (Tibi-Server + tibi-admin) entwickelst du nicht weiter, du nutzt es.
Verifiziert gegen: Starter-`README.md`, Skill `tibi-project-setup/SKILL.md`, `Makefile`, `docker-compose-local.yml`, `api/config.yml`. Stand: am Code geprüft, nicht aus Alt-Doku.
## Setup-Checkliste
```text title="Setup-Checkliste"
[ ] Geklont, `origin` zeigt aufs Projekt-Repo (`template` heißt das Starter-Remote)
[ ] `__PROJECT_NAME__` (kebab-case) und `__TIBI_NAMESPACE__` (snake_case) ersetzt
[ ] `__ORG__` / `__PROJECT__` ersetzt, kein `__*__` mehr per `rg` findbar
[ ] `.env`-URLs gesetzt (`CODING_URL`, `CODING_TIBIADMIN_URL`, `STAGING_URL`, `LIVE_URL`)
[ ] `api/config.yml.env` mit frischem `ADMIN_TOKEN` + `ADMIN_ASSET_VERSION`
[ ] `App.svelte` hat `<svelte:head>` mit `<title>`
[ ] `yarn install`, `make docker-up`, Website + Admin + API antworten
[ ] `yarn validate`, `yarn build`, `yarn build:server` fehlerfrei
```
## 1. Klonen und Remotes
```sh title="Platzhalter prüfen"
git clone https://gitbase.de/cms/tibi-svelte-starter.git mein-projekt
cd mein-projekt
git remote rename origin template
git remote add origin https://gitbase.de/<org>/<repo>.git
```
## 2. Platzhalter und Starter-Reste
Ersetzen in `.env`, `api/config.yml`, `api/hooks/config-client.js`, `package.json`, `README.md` und `frontend/.htaccess` (falls Apache-Rewrite genutzt wird):
```sh title="Platzhalter ersetzen"
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
```
`package.json` darf danach nicht mehr aufs Starter-Repo zeigen. `PRODUCTION_PATH`, `SENTRY_PROJECT` und `TIBI_NAMESPACE` projektspezifisch setzen.
## 3. Welcher Betriebs-Pfad?
**Pfad A — lokaler Starter-Stack (Standard):** `make docker-up` startet `docker-compose-local.yml` mit Profil `tibi`. Das Projekt wird als `/data` in `tibiserver` gemountet, `DB_DIAL`, `DB_PREFIX`, `MAIL_HOST` kommen aus Container-Env. Traefik-URLs:
- Website: `https://<projekt>.code.testversion.online/`
- Admin: `https://<projekt>-tibiadmin.code.testversion.online/`
- Roh-Server: `https://<projekt>-tibiserver.code.testversion.online/` (falls Profil aktiv)
- Maildev: `https://<projekt>-maildev.code.testversion.online/`
**Pfad B — geteilter/fremder tibi-server:** nur wenn die Umgebung das ausdrücklich verlangt (eigene Server-Config, explizite Projekt-Registrierung). Dann vorab klären: welches Admin-Token für `/api/v1/project`, welche Basis-URL, wie der Projektpfad gemountet ist. Keine Pfad-B-Schritte im lokalen Starter-Stack erfinden.
## 4. Dateien sind nicht gleich registriert
Dateien auf Platte definieren das Projekt. Erst die **Registrierung** macht es dem laufenden tibi-server bekannt. Bis dahin antwortet der Namespace mit `project not found` — das ist dann kein Proxy-Fehler.
## 5. Registrieren (wenn `project not found`)
Im lokalen Standard-Setup per `admin` / `admin` einloggen und JWT nutzen:
```sh title="JWT holen und Projekt registrieren"
jwt=$(curl -s -X POST "$CODING_TIBIADMIN_URL/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/project" \
-H "Content-Type: application/json" \
-H "X-Auth-Token: $jwt" \
-d '{"name":"<PROJECT_NAME>","description":"...","configFile":"/data/api/config.yml","enabled":true}'
```
Danach erscheint das Projekt unter `GET /api/v1/project`, und `/_/<namespace>/...` löst auf. Nach Config-Änderungen neu laden:
```sh title="Projekt-Reload anstoßen"
curl -s -X POST "$CODING_TIBISERVER_URL/api/v1/_/<PROJECT_NAME>/_/admin/reload" \
-H "X-Auth-Token: $jwt"
```
:::caution title="Token nicht verwechseln"
Verwechsle `ADMIN_TOKEN` aus `api/config.yml.env` nicht mit System-Rechten: er schreibt Sammlungen (Kopf `Token`), registriert aber keine Projekte. Deploy-Skripte nutzen ihn separat als `Authorization: Bearer` auf dem Projekt-Reload.
:::
## 6. Inhalte und URLs verstehen
- `api/config.yml` definiert `namespace`, `collections` (Inhalte, Medien, Navigation, Tags, Kommentare, SSR-Cache), `assets` (`dist`, `fonts`, `img`) und `admin.tokens`.
- Frontend: Svelte-SPA in `frontend/src/` (`App.svelte`, `routes/`, `blocks/`, `widgets/`). `yarn build` baut Browser-Bundle plus Admin-Modul, `yarn build:server` das SSR-Bundle für `api/hooks/lib/app.server.js`.
- Medien-URLs folgen `/api/medialib/<entryId>/file/<name>?filter=...`, gerendert nur über `MedialibImage`.
- Öffentliche Abfragen brauchen `active:true` — auch bei Navigation und Medien, sonst bleibt die Navigation leer, obwohl Lesen mit Token klappt.
<FileTree>
- api/
- config.yml
- hooks/lib/app.server.js
- frontend/src/
- App.svelte
- routes/
- blocks/
- widgets/
</FileTree>
## 7. Abnahme
```sh title="Abnahme: Build und Erreichbarkeit"
yarn validate
yarn build
yarn build:server
curl -I "$CODING_URL"
curl -I "$CODING_TIBIADMIN_URL"
```
Anonyme SSR-Seite prüfen (Navigation vorhanden, Bilder mit `_lookup`), nicht nur Lesen mit Token. Erst dann ist das Projekt wirklich aufgesetzt.
+73 -27
View File
@@ -1,48 +1,94 @@
---
title: Schnellstart
description: Tibi-Doku lokal lesen und Tibi-Server plus Admin lokal starten.
description: Eigenes Tibi-Projekt aus dem Starter klonen, starten und prüfen — für Projekt-Entwickler.
---
## Diese Doku lokal lesen
Du entwickelst **Projekte mit Tibi**, nicht Tibi selbst. Go-Code vom tibi-server und Svelte-Code vom tibi-admin fasst du dafür nicht an. Dein Startpunkt ist immer der Starter.
```sh
cd /WM_Dev/src/gitbase.de/cms/tibi-docs
npm install
npm run dev
# → http://localhost:4321
:::danger title="Kern-Regel aus dem Starter"
Dev-Server laufen immer in Docker. Nie `yarn dev` oder `yarn start` lokal.
:::
## Voraussetzungen
`git`, `yarn`, `make`, `docker compose`, `curl`. Arbeit auf dem Code-Server mit `*.code.testversion.online`.
## 1. Klonen
```sh title="Starter klonen"
git clone https://gitbase.de/cms/tibi-svelte-starter.git mein-projekt
cd mein-projekt
git remote rename origin template
git remote add origin https://gitbase.de/<org>/<repo>.git
```
Oder über den Docker-Stack (Traefik):
## 2. Platzhalter ersetzen
```sh
make docker-up
# → https://tibi-docs.code.testversion.online/
make docker-down
In `.env`, `api/config.yml`, `api/hooks/config-client.js`, `package.json` und `frontend/.htaccess` (falls genutzt):
- `__PROJECT_NAME__` → kebab-case, z. B. `mein-projekt`
- `__TIBI_NAMESPACE__` → snake_case, z. B. `mein_projekt`
- `__ORG__`, `__PROJECT__` → Gitea-Org und Repo
Prüfen:
```sh title="Platzhalter prüfen"
rg '__[A-Z0-9_]+__' . --glob '*.{yml,js,env,htaccess,json,md,ts,svelte}'
```
## Tibi-Server lokal starten
Es darf nichts mehr finden. Sonst ist das Setup nicht fertig.
```sh
cd ../tibi-server
cp config.yml.example config.yml # falls nötig
docker compose -f docker-compose-local.yml up -d
go run cmd/server/main.go
## 3. Token erzeugen
```sh title="Token erzeugen"
token=$(openssl rand -hex 20)
cat > api/config.yml.env <<EOF
ADMIN_TOKEN=$token
ADMIN_ASSET_VERSION=$(node -e "process.stdout.write(require('crypto').randomBytes(6).toString('hex'))")-dirty-$(date +%s)
EOF
```
## Tibi-Admin-Nova lokal starten
Trage in `.env` die Projekt-URLs ein: `LIVE_URL`, `CODING_URL`, `STAGING_URL`, `CODING_TIBIADMIN_URL`.
```sh
cd ../tibi-admin-nova
## 4. Starten
```sh title="Stack starten"
yarn install
make docker-up
# → https://tibi-admin-nova.code.testversion.online/
```
## Doku pflegen
Vordergrund bei Problemen: `make docker-start`. Logs: `make docker-logs`.
Server-Docs nach Änderungen synchronisieren:
## 5. Erreichbarkeit prüfen
```sh
npm run sync
npm run dev # prüfen
```sh title="Erreichbarkeit prüfen"
curl -I "$CODING_URL"
curl -I "$CODING_TIBIADMIN_URL"
curl -I "$CODING_URL/api/content?limit=1"
```
- HTML statt JSON unter `/api/...` → Proxy/Setup noch falsch.
- `project not found` → Projektdateien liegen da, aber das Projekt ist im laufenden tibi-server noch nicht registriert. Siehe [Neues Projekt](/einstieg/neues-projekt/) Schritt 5.
Anmeldung im Admin lokal: `admin` / `admin`.
## 6. Bauen und validieren
```sh title="Bauen und validieren"
yarn validate
yarn build
yarn build:server
```
Alle drei müssen fehlerfrei durchlaufen. `yarn` nutzt du nur für diese drei — nie für Dev-Server.
## Token-Köpfe (nicht vermischen)
| Wofür | Kopf | Wert |
|---|---|---|
| Sammlung lesen/schreiben (z. B. Inhalte) | `Token` | `ADMIN_TOKEN` aus `api/config.yml.env` |
| Eigener Benutzer (JWT) | `X-Auth-Token` | Token aus `POST /api/v1/login` |
| Projekt anlegen/laden (System) | `X-Auth-Token` (JWT als `admin`) oder `X-Admin-Token` | nur wenn Server-Token vorhanden |
| Deploy-Reload auf Live/Staging | `Authorization: Bearer` | `ADMIN_TOKEN` |
Weiter: [Neues Projekt aufsetzen](/einstieg/neues-projekt/) mit Registrierung, Pfad A/B und Checkliste.
+21 -12
View File
@@ -1,27 +1,36 @@
---
title: Was ist Tibi?
description: Überblick über Tibi-Server, Tibi-Admin-Nova und Tibi-Projekte.
description: Überblick über Tibi-Server, Tibi-Admin und Tibi-Projekte.
---
**Tibi** ist unser Headless-CMS-Baukasten:
import { Card, CardGrid } from '@astrojs/starlight/components';
- **Tibi-Server** (Go + MongoDB): konfigurationsgetriebenes API-Backend. Collections werden in YAML definiert, Logik läuft über JavaScript-Hooks, Echtzeit über SSE-Kanäle. Dazu LLM-Anbindung, Dateiupload/Bildverarbeitung, PDF, Audit-Log, Cron-Jobs, OpenAPI und Prometheus-Metriken.
- **Tibi-Admin-Nova** (Svelte 5 + esbuild): das Admin-Frontend. Läuft lokal im Mock-Modus und in Staging/Live statisch hinter einem Reverse Proxy gegen den echten Tibi-Server.
- **Tibi-Projekt** (z. B. aus `tibi-svelte-starter`): Kunden-Website als Svelte-SPA mit SSR-Hook im Tibi-Server.
**Tibi** ist unser Baukasten aus Verwaltungs-Backend plus eigenständigen Projekt-Websites:
- **Backend = Tibi-Server + tibi-admin.** Der **Tibi-Server** (Go + MongoDB) ist das API-Backend: Collections in YAML, Logik über JavaScript-Hooks, Echtzeit über SSE, dazu LLM, Upload/Bildverarbeitung, PDF, Audit, Jobs, OpenAPI. **tibi-admin** (Release Nova) ist die Bedienoberfläche zu diesem Backend: Svelte-Frontend, lokal im Mock-Modus, in Staging/Live statisch hinter Reverse Proxy gegen den echten Server.
- **Projekt-Website (aus `tibi-svelte-starter` geklont):** eigenständige Website auf Tibi-Basis — normale kleine Websites, Shops, Intranet-Anwendungen usw. Svelte-SPA mit SSR-Hook im Tibi-Server. **Hier arbeitest du als Projekt-Entwickler** — du klonst den Starter, ersetzt Platzhalter und konfigurierst Sammlungen und Hooks. Go-Code vom Server und Admin-Code fasst du nicht an.
## Wie hängt das zusammen?
```text
Browser (Nova-Admin / Kunden-SPA)
│ REST /api/v1, SSE
```text title="Zusammenspiel im Überblick"
Projekt-Website (eigenständig: Website, Shop, Intranet aus tibi-svelte-starter)
│ Svelte-SPA, SSR-HTML aus dem Server-Hook
▼
Tibi-Server (Go, config.yml + collections/*.yml + hooks/*.js)
│ MongoDB, Mail, Dateien, LLM-Upstream
Backend: tibi-admin (Bedienoberfläche) ──REST /api/v1, SSE──▶ Tibi-Server (Go)
│ config.yml + collections/*.yml + hooks/*.js
▼
MongoDB / Maildev / Storage
```
Backend verwalten, Website bauen: als Projekt-Entwickler pflegst du Inhalte im Admin und baust die eigenständige Website im Starter.
## Wo geht es weiter?
- Schnellstart: [Einstieg/Schnellstart](/einstieg/schnellstart/)
- Begriffe: [Einstieg/Begriffe](/einstieg/begriffe/)
<CardGrid stagger>
<Card title="Schnellstart">
Starter klonen, starten, prüfen. [Loslegen](/einstieg/schnellstart/).
</Card>
<Card title="Begriffe">
Die wichtigsten Tibi-Begriffe im Glossar. [Nachschlagen](/einstieg/begriffe/).
</Card>
</CardGrid>
+20 -7
View File
@@ -1,6 +1,6 @@
---
title: Tibi Doku
description: Entwickler- und Anwender-Dokumentation für Tibi-Server und Tibi-Admin-Nova.
description: Entwickler- und Anwender-Dokumentation für Tibi-Server und Tibi-Admin.
template: splash
hero:
tagline: Headless-CMS mit Go-Backend und Svelte-Frontends — hier steht, wie man damit baut.
@@ -17,16 +17,29 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
## Wo finde ich was?
> **Im Aufbau.** Diese Doku wird gerade sauber neu strukturiert. Fertig ist der [Einstieg](/einstieg/was-ist-tibi/) — Server-, Nova-, Anwender- und Referenz-Seiten folgen Schritt für Schritt.
<CardGrid stagger>
<Card title="Was ist Tibi?">
Überblick über Tibi-Server, Tibi-Admin-Nova und Tibi-Projekte. [Lesen](/einstieg/was-ist-tibi/).
Backend plus eigenständige Projekt-Websites im Überblick. [Lesen](/einstieg/was-ist-tibi/).
</Card>
<Card title="Schnellstart">
Doku und Stacks lokal starten, Doku pflegen. [Starten](/einstieg/schnellstart/).
Starter klonen, starten, prüfen — in 6 Schritten. [Starten](/einstieg/schnellstart/).
</Card>
<Card title="Begriffe">
Namespace, Collection, Hook und Co. im Glossar. [Nachschlagen](/einstieg/begriffe/).
<Card title="Neues Projekt">
Vom Klon zum registrierten Projekt inkl. Checkliste. [Aufsetzen](/einstieg/neues-projekt/).
</Card>
<Card title="Projekt bauen">
Sammlungen, Felder, Hooks, Suche, SSR, Pagebuilder, LLM, MCP, Deploy. [Bauen](/projekt/sammlungen-anlegen/).
</Card>
<Card title="Server betreiben">
Konfiguration, Betrieb, Sicherheit, Schnittstellen. [Betreiben](/server/uebersicht/).
</Card>
<Card title="Admin bedienen">
Ansichten, Editor, Medien, Exporte, Aktionen, Rechte, KI. [Verwalten](/admin/konzept/).
</Card>
<Card title="Anwender-Anleitung">
Anmelden bis Sichern — für Nicht-Profis mit Bildern. [Anleiten](/anwender/anmelden/).
</Card>
<Card title="Referenz">
Schemas, Hook-Kontexte, Token-Köpfe, Fehlerbilder, OpenAPI. [Nachschlagen](/reference/schemas/).
</Card>
</CardGrid>
+143
View File
@@ -0,0 +1,143 @@
---
title: 'Lesen und suchen'
description: 'Filtern, Sortieren, Blättern, Projektionen, Lookups, Facetten, Suche.'
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Lesen geht per `GET` auf eine Sammlung.
Filtern, Sortieren, Blättern und Auflösen hängst du als Query-Parameter an.
Basis: `GET /api/v1/_/<namespace>/<collection>`.
## GET-Liste
<Tabs>
<TabItem label="Liste">
```text title="Liste mit Filter, Sortierung und Blättern"
GET /api/v1/_/<namespace>/content?filter={"lang":"de"}&sort=-name&limit=20&offset=0
```
Liste mit `filter`, `sort`, `limit`, `offset`.
</TabItem>
<TabItem label="Einzel">
```text title="Einzelner Eintrag per ID"
GET /api/v1/_/<namespace>/content/<id>
```
Einzelner Eintrag per ID.
</TabItem>
</Tabs>
<Aside type="caution">
`?count=1` liefert zusätzlich die Header `X-Results-Count` und — Achtung, Tippfehler im Code — `Content-Range:` **mit Doppelpunkt** (`collection_get.go`).
</Aside>
## Filtern, Sortieren, Blättern
| Parameter | Format | Beispiel |
|---|---|---|
| `filter` | MongoDB-Filter als JSON | `?filter={"lang":"de","active":true}` |
| `sort` | Feld, `-` heißt absteigend; Komma oder wiederholt | `?sort=-name`, `?sort=type&sort=-language` |
| `limit` | Zahl, max. Einträge | `?limit=20` |
| `offset` | Zahl, übersprungene Einträge | `?offset=40` |
| `count` | `1` oder `true` | `?count=1` |
Filter-Operatoren wie `$and`, `$or`, `$in`, `$lte`, `$gte`, `$exists` funktionieren.
Der Starter nutzt sie selbst im `publishedFilter` (`api/hooks/config.js`): `active: true` plus Veröffentlichungsfenster `publication.from`/`publication.to`.
```text title="Nächste Seite, neueste zuerst"
# nächste Seite, neueste zuerst (Annahme: Feld updatedAt existiert)
GET …/comment?filter={"contentId":"<id>"}&sort=-updatedAt&limit=10&offset=10
```
## select und projection
`select` begrenzt die Felder — Komma-getrennt oder wiederholt.
Das spart Bandbreite und Kontext, vor allem bei Dateien.
```text title="Felder per select begrenzen"
GET …/medialib?select=title,alt,tags
GET …/content?select=name&select=path&select=lang
```
- `select` ohne `file.data` abfragen: Datei-Inhalte (`file.data`) sind groß — `file.src` reicht für Anzeige.
- Der Starter-Hinweis dazu: Liste mit `select` ohne `file.data` lesen, sonst bläht der Kontext auf.
- `projection` wählt eine vordefinierte Projektion aus der Sammlungs-YAML (`projections`-Block).
- Unbekannte Felder oder fremde Projektionen → `400`.
## Lookup: Verweise auflösen
`lookup` löst Fremdschlüssel auf und legt das Ergebnis unter `_lookup` ab.
Format: `feld:sammlung`, Rest mit Doppelpunkten (`feld:sammlung:fremdfeld:projektion:select:als`).
```text title="Verweise per lookup auflösen"
# Navigation mit Seitendaten (Feld page -> content)
GET …/navigation?lookup=elements.page:content
# Content mit Hero- und Richtext-Bildern (Felder aus content.yml)
GET …/content?lookup=blocks.heroImage.image:medialib,blocks.image:medialib
```
- Ohne `lookup` steht im Feld nur die ID (z. B. `page: "<content.id>"`).
- Mit `lookup` steht der Zieldatensatz in `_lookup.page` (Name, Pfad usw.).
- Die App baut daraus den Link: `href` aus `_lookup.page.path` plus `hash`.
- Mehrere Lookups per Komma oder wiederholtem `?lookup=`.
- Ziel ohne Leserecht oder unbekannte Sammlung → Fehler (`lookup permission denied`, Ziel nicht gefunden).
<Aside type="caution">
Ein `lookup` löst nur auf, wenn der Zieldatensatz für dich lesbar ist.
Fehlt dort `active:true`, bleibt `_lookup` öffentlich leer — Bilder und Navigation verschwinden.
Details: [Rechte und Tokens](/projekt/rechte-tokens/).
</Aside>
## Facetten und Zählen (eigene Endpunkte!)
<Aside type="caution">
`_count` und `_facet` sind eigene Routen — **nicht** Query-Parameter der Liste.
`?facet=` an der normalen Liste wird still ignoriert, und `…/comment/count` landet auf der Einzel-Route (`:id = "count"`) statt zu zählen.
</Aside>
| Endpunkt | Wirkung | Beispiel |
|---|---|---|
| `GET …/<collection>/_count?filter={…}` | Nur Anzahl, keine Einträge | `GET …/comment/_count?filter={"contentId":"<id>"}` |
| `GET …/<collection>/_count?estimate=1` | Schnelle Schätzung statt exakter Zahl (nur **ohne** `filter`) | `GET …/comment/_count?estimate=1` |
| `GET …/<collection>/_facet?facet=<feld>` | Werte zählen je Feld | `GET …/comment/_facet?facet=contentId` |
| `…/_facet?facet=<feld>&limit=10` | Nur Top N | `GET …/comment/_facet?facet=contentId&limit=10` |
| `…/_facet?facet=<feld>&includeEmpty=1` | Leere Werte mitzählen | `GET …/comment/_facet?facet=tags&includeEmpty=1` |
Die Kommentar-Anzahl je Seite (`commentCount`) kommt aus so einer Abfrage über `contentId`.
## Suche
`q` sucht, `qName` wählt den Suchindex aus dem `search`-Block der Sammlung.
```text title="Suche mit q und qName"
# kombinierte Suche in content (RRF aus semantisch + fuzzy)
GET …/content?q=kontakt&qName=combined
# einfache Suche in Tags (Default-Index, regex)
GET …/tag?q=hero
```
| `qName` | Sammlung | Modus |
|---|---|---|
| `combined` | `content` | Hybrid (RRF, `k: 60`, `topK: 100`) |
| `fuzzy` | `content` | n-gram, fehlertolerant |
| `semantic` | `content` | Vektor (`bge_m3`) |
| `default` | `tag`, `navigation`, `comment`, `medialib` | regex |
`?qExplain=1` erklärt, wie die Suche gewichtet hat — nützlich beim Debuggen.
## Fehlerbilder
| Antwort | Ursache | Lösung |
|---|---|---|
| `400` | Ungültiges `filter`-JSON, unbekanntes `select`-Feld, falsche `lookup`-Syntax | JSON prüfen, Feldnamen gegen die Sammlungs-YAML halten |
| `permission denied` | Kein `get`-Recht für deine Rolle | [Rechte und Tokens](/projekt/rechte-tokens/) prüfen |
| `invalid projection` | `projection` heißt anders als in der YAML | Namen im `projections`-Block nachschlagen |
| Leere Liste trotz Daten | `filter_public`-Hook filtert `active:true` + Zeitraum | `active:true` setzen, `publication`-Fenster prüfen |
| `_lookup` leer | Zieldatensatz nicht lesbar (meist `active:true` fehlt) | Zieldatensatz prüfen, nicht die Abfrage |
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter), am 68b42785 2026-10-15 (tibi-server, collection_get/select/sort/facet/count/lookup).
+145
View File
@@ -0,0 +1,145 @@
---
title: 'Änderungen und Sicherung'
description: 'Audit-Log lesen, Snapshots nutzen, Backup und Restore mit dem Betreiber abstimmen.'
---
import { Aside, Steps, Card, CardGrid } from '@astrojs/starlight/components';
Wer hat was geändert — und wie kommst du zurück?
Drei Ebenen: Das Audit-Log protokolliert Änderungen, Snapshots frieren Datenstände ein, Backups sichern alles für den Ernstfall.
Suchbegriffe: Änderungshistorie, Verlauf, Wiederherstellen, Snapshot, Datensicherung.
<CardGrid>
<Card title="Audit-Log">Protokolliert Änderungen — wer, wann, vorher/nachher.</Card>
<Card title="Snapshots">Frieren Datenstände im Ordner `snapshotPath` ein.</Card>
<Card title="Backups">Voll-Export als ZIP für Umzug und Ernstfall.</Card>
</CardGrid>
## Audit einschalten und lesen
Audit ist zweistufig: Es läuft nur, wenn **Server und Projekt** es erlauben.
```yaml title="api/config.yml — Projekt-Opt-in (Starter-Standard)"
audit:
enabled: true
```
| Ebene | Schalter | Standard |
|---|---|---|
| Server | `audit.enabled`, TTL `audit.defaultTTL: 720h` (30 Tage, `"0"` = ewig) | Betreiber-Sache |
| Projekt | `audit.enabled: true` | Starter: an |
| Sammlung | `audit: { enabled, actions: [create, update, delete] }` | Standard-Aktionen: anlegen, ändern, löschen |
| Action | `audit: { enabled, actions: [get, post] }` | Nur was du einschaltest |
<Aside type="note">
Ohne `audit.enabled: true` im Projekt bleibt das Log dunkel — auch wenn der Server es anbietet.
Die Sammlung `ssr` (Cache) ist bewusst ausgenommen (`enabled: false`).
</Aside>
<Steps>
1. Etwas ändern (z. B. Seitentitel per `PUT`).
2. Im **tibi-admin** die Änderungshistorie des Eintrags öffnen — wer, wann, vorher/nachher.
3. Alte Fassung per Wiederherstellen zurückschreiben (erst an Testdaten üben).
4. Per API lesen geht mit `GET /api/v1/audit` (Recht `audit.read` nötig).
</Steps>
## Verlauf im Admin
Du musst nicht per API lesen: Der Admin zeigt den Verlauf an zwei Stellen — pro Eintrag im Editor und global als „Audit-Log“ (`/admin/audit` bzw. `/projects/:id/audit`, mit Filtern für Projekt, Collection, Aktion und Benutzer, 25 Zeilen pro Seite).
**Verlaufspanel pro Eintrag.** Im Editor lädt das Panel die letzten 50 Änderungen dieses Datensatzes, neueste zuerst. Jede Zeile zeigt Zeit, Aktion („erstellt“, „geändert“, „gelöscht“), Person und Quelle; bei „geändert“ steht zusätzlich, welche Felder betroffen sind. Ein Pfeil klappt die Zeile auf und zeigt den Vorher/Nachher-Vergleich. Bei „geändert“ und „gelöscht“ erscheint — nur mit Schreibrecht — der Schalter „Wiederherstellen“, der den alten Stand ins Formular übernimmt (danach noch speichern).
**Diff-Ansicht.** Jeder Vergleich zeigt pro Feld eine Karte mit Status-Badge: „hinzugefügt“, „entfernt“ oder „geändert“. Geänderte Texte stehen als „Vorher“ und „Nachher“ nebeneinander, bei kurzen Texten sind einzelne Wörter markiert. Lange Werte sind eingeklappt und lassen sich per Pfeil ganz aufklappen; jeder Wert hat einen Kopieren-Schalter. Feldnamen kommen aus der Sammlungs-Definition, der technische Pfad steht daneben. Gibt es nichts zu zeigen, meldet die Ansicht „Keine Änderungen“.
**Wiederherstellen-Dialog.** Der Dialog öffnet sich als seitliches Panel („Wiederherstellen“) und stellt „Aktuell“ (aus dem geöffneten Formular) und „Danach“ (aus dem Verlauf) gegenüber. Per Haken wählst du einzelne Felder aus — „Alle auswählen“ und „Keine“ helfen, der Zähler (z. B. 3 / 5) zeigt den Stand. Beachte den Hinweis „Media-Dateien werden nicht wiederhergestellt“. Bestätigen übernimmt die gewählten Felder ins Formular („Felder übernommen — bitte speichern“).
**Seitenleisten-Verlauf.** Unter dem Editor zeigt eine kompakte Liste die letzten 10 Änderungen mit Zeit, Aktion und Person. Sie blendet sich bei leerem Verlauf komplett aus — kein Titel, kein Hinweistext. Ein Klick auf eine Zeile öffnet den vollen Vergleich, bei mehr als 10 Einträgen führt „Ältere Einträge“ zur Gesamtansicht. Nach dem Speichern lädt die Liste automatisch neu.
## Snapshots
Snapshots sind eingefrorene Datenstände im Ordner `snapshotPath` (`../snapshots` im Starter):
```yaml title="api/config.yml — Snapshot-Profile des Starters"
snapshotPath: ../snapshots
snapshots:
- name: daily # 12:00 Uhr, redaktionelle Daten + Medien, 14 Stück
schedule: "0 12 * * *"
collections: [content, navigation, tags, medialib, ssr]
includeMedia: true
retention: { maxCount: 14 }
- name: full-daily
schedule: "0 3 * * *" # 03:00 Uhr, alles + Medien, 14 Stück
includeMedia: true
retention: { maxCount: 14 }
- name: full-weekly # sonntags 05:00 Uhr, 12 Stück
schedule: "0 5 * * 0"
collections: [] # leer = alle Sammlungen
includeMedia: true
retention: { maxCount: 12 }
- name: full-monthly # monatlich, Langzeitarchiv, 12 Stück
schedule: "0 6 1 * *"
collections: []
includeMedia: true
retention: { maxCount: 12 }
```
| Regel | Bedeutung |
|---|---|
| `collections: []` (leer) | Alle Sammlungen sichern |
| Liste mit Namen | Nur diese sichern (Medien nur mit `includeMedia: true`, per Hardlink) |
| `retention.maxCount` | Älteste fliegen automatisch raus |
| Speicherung | `json` (BSON-Extended-JSON auf Platte, Standard); `db`/`both` sind Zukunft |
<Aside type="caution" title="Typo im Starter: tags statt tag">
Das `daily`-Profil oben ist vereinfacht aus der Starter-`config.yml` abgeschrieben (`description`/`enabled` weggelassen) — aber der Name `tags` darin steht wirklich so im Starter, obwohl die Sammlung `tag` heißt (siehe `api/collections/tag.yml`, `name: tag`). Übernimm die Liste nicht blind: prüfe jeden Namen gegen deine `api/collections/*.yml`, sonst sichert das Profil eine Sammlung, die es nicht gibt.
</Aside>
Im **tibi-admin** (`/projects/:id/snapshots`, drei Tabs):
| Tab | Was du tust |
|---|---|
| Configs | Profile ansehen |
| Entries | Auslösen (mit Fortschritt), herunterladen (ZIP), zurückspielen, löschen, aufräumen |
| Stats | Belegung prüfen |
Zum Zurückspielen braucht dein Token `AllowSnapshot: true`.
Übe das Zurückspielen **nur mit Testdaten** — es überschreibt den Live-Stand.
## Backup und Restore (mit Betreiber)
Backups sind der Voll-Export für Umzug und Desaster-Fall — im Admin unter `/projects/:id/backup`:
<Steps>
1. Export-Tab: Sammlungen wählen (Kopf-Kästchen = alle), „Start backup“ klicken, Fortschritt abwarten, Datei mit Größenangabe laden.
2. Bricht der Download ab: erst Analyse (`backup/analyze-download`), dann neu laden.
3. Import-Tab: ZIP in die Zone ziehen, Analyse lesen, Restore bestätigen.
4. Danach: [Reload und SSR-Cache](/projekt/deploy/) prüfen, Seite anonym aufrufen.
</Steps>
| Unterschied | Snapshot | Backup |
|---|---|---|
| Wofür | Schneller Rücksprung im Alltag | Umzug, Desaster, Weitergabe |
| Wo | `snapshotPath` auf dem Server | ZIP-Datei bei dir |
| Recht | `AllowSnapshot` | Projekt-Admin plus `AllowBackup` |
| Fortschritt | Ja (Stream) | Ja (SSE-Stream) |
<Aside type="caution">
Backup und Restore immer mit dem Betreiber abstimmen: Restore überschreibt Daten, große Medien brauchen Zeit und Platz.
Fortschritt läuft als Stream (`text/event-stream`) — bei Abbrüchen nichts doppelt starten, sondern Analyse neu holen.
</Aside>
## Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Audit bleibt leer | `audit.enabled` im Projekt fehlt oder Server-Flag aus | Beide Schalter prüfen, Änderung erneut auslösen |
| Alte Einträge verschwinden | TTL (`720h`) abgelaufen | Wichtige Stände als Snapshot sichern, TTL ggf. auf `"0"` |
| Snapshot-Button fehlt | Token ohne `AllowSnapshot` | Betreiber um Recht bitten |
| Backup-Button fehlt | Kein Projekt-Admin oder `AllowBackup` fehlt | Betreiber um Recht bitten |
| Restore zeigt keine Wirkung | Cache alt | Reload plus `?clear=1` (siehe [Veröffentlichen](/projekt/deploy/)) |
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter, config.yml audit- und snapshots-Blöcke), am 68b42785 2026-10-15 (tibi-server, audit-, backup- und snapshot-docs), am 2b091d89 2026-10-13 (tibi-admin-nova, test-pläne 43 und 44), am 2b091d89 2026-10-13 (tibi-admin-nova, AuditView, AuditHistoryPanel, AuditDiff, AuditRestoreDialog, AuditSidebarHistory).
+109
View File
@@ -0,0 +1,109 @@
---
title: 'Dateien und Bilder'
description: 'Upload-Format, Medialib-Speicherung, Anzeige nur via MedialibImage, Bildfilter.'
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
Dateien liegen in der Sammlung `medialib`.
Du lädst sie per `POST` hoch, verknüpfst sie per ID und zeigst sie nur über die Komponente `MedialibImage` an.
Suche-Begriffe: Datei hochladen, Bild anzeigen, Bildfilter.
## Upload-Format
<Steps>
1. Lege einen `medialib`-Eintrag mit einem `file`-Objekt an — `src` als Data-URI, `path` als Dateiname:
```text title="Upload per POST (Data-URI)"
POST /api/v1/_/<namespace>/medialib
{"title": "Demo Hero", "active": true, "file": {"src": "data:image/jpeg;base64,…", "path": "demo-hero-1200.jpg"}}
```
2. Prüfe die Antwort: `file.src` beginnt danach mit `<feld>/`, zum Beispiel `file/demo-hero-1200.jpg`.
3. Lies Listen immer ohne Datei-Bytes:
```text title="Liste ohne Datei-Bytes lesen"
GET …/medialib?select=title,alt,tags
```
</Steps>
| Richtig | Falsch |
|---|---|
| `{src: 'data:…;base64,…', path: 'name.jpg'}` (`path` optional) | `{filename, data}`-Format |
| `file` als Objekt | `file` als reiner String |
| `file.src` nach dem Speichern beginnt mit `file/` | `?filter=` an Data-URIs hängen |
Grenzen aus `medialib.yml`: `file` ist Pflicht, max. `50MB`, Downscale auf max. `2048` px beim Upload (`downscale.maxWidth/maxHeight`).
## Speicherformat
Gespeichert wird `{src: '<feld>/name.jpg', path, type}` — also relativer Pfad plus erkannter MIME-Typ.
Die Widgets `file`, `file[]` und `foreignFile` schreiben dieses Format; `foreignMedia` in `content.yml` (`blocks.heroImage.image`, `blocks.image`) verweist per Medialib-ID darauf.
<Aside type="caution" title="active:true nicht vergessen">
`filter_public.js` erzwingt anonym `active:true` — `medialib.yml` deklariert aber kein `active`-Feld.
Ohne gespeichertes `active:true` löst **kein** öffentlicher Lookup auf (Vorfall 2026-10-05).
Speichere `active:true` auf jedem Medialib-Dokument.
</Aside>
## Anzeige-Regel: nur MedialibImage
Baue Bild-URLs nie von Hand. Die Komponente `frontend/src/widgets/MedialibImage.svelte` nimmt `id` oder `entry`, löst `file.src` auf und wählt den Filter nach Breite.
```text title="Bild-URL mit Filter (Beispiel)"
/api/medialib/<entryId>/file/<name>?filter=l-webp
```
So nutzen es die Blöcke: `HeroBlock.svelte` löst `block.heroImage?._lookup?.image` (Fallback `block._lookup["heroImage.image"]`) auf und rendert bei fehlendem Bild einen Verlauf statt eines Bildes. `RichtextBlock.svelte` nutzt `block._lookup?.image` — ohne aufgelösten Lookup bleibt das Bild unsichtbar.
<Tabs>
<TabItem label="Hero (Content)">
```text title="Hero-Lookups (immer gemeinsam)"
GET …/content?lookup=blocks.heroImage.image:medialib,blocks.image:medialib
```
Beide Lookups immer gemeinsam anfordern (Empfehlung aus `content.yml`).
</TabItem>
<TabItem label="Navigation / Medialib-Liste">
```text title="Navigation und Medialib-Liste"
GET …/navigation?lookup=elements.page:content
GET …/medialib?select=title,alt,tags
```
</TabItem>
</Tabs>
## Filter
Die Größen stehen in `api/collections/lib/imageFilter.yml` (`imageFilter`-Block in `content.yml` und `medialib.yml`):
| Filter | Breite | Qualität | Ausgabe |
|---|---|---|---|
| `xs-webp` | 100 px (100×100, `fit`) | 60 | webp |
| `s-webp` | 300 px | 70 | webp |
| `m-webp` | 600 px | 76 | webp |
| `l-webp` | 1200 px | 80 | webp |
| `xl-webp` | 2000 px | 84 | webp |
| `xxl-webp` | 2800 px | 88 | webp |
`MedialibImage` wählt automatisch (`minWidth`-Messung, Standard `l-webp`); du kannst `filter` auch fest setzen, z. B. `?filter=l-webp`.
`?filter=` wirkt nur auf gespeicherte Dateien per URL — nicht auf Data-URIs.
## Fehlerbilder
| Symptom | Ursache | Lösung |
|---|---|---|
| Nur IDs, kein `_lookup` | `lookup=…:medialib` fehlt in der Abfrage | Lookup-Parameter ergänzen (siehe [Lesen und suchen](/projekt/abfragen-lesen/)) |
| `_lookup` öffentlich leer, mit Token voll | `active:true` fehlt am Medialib-Dokument | `active:true` speichern |
| Hero zeigt Verlauf statt Bild | Lookup unaufgelöst (`file.src` fehlt) | Eintrag + `active:true` + Lookup prüfen |
| Richtext-Bild verschwindet öffentlich | Gleicher Grund | Gleicher Fix |
| Riesige Antworten | `file.data` mitgelesen | `select` ohne Dateifelder nutzen |
| Dateien bleiben nach Löschen übrig? | Passiert nicht von selbst | Beim Löschen räumt der Server das Upload-Verzeichnis des Dokuments automatisch weg (`filecleanup`, unter der Projekt-UID) — Waisen prüfst du per Dateisuche im Upload-Pfad |
Weiter: [Lesen und suchen](/projekt/abfragen-lesen/), [Rechte und Tokens](/projekt/rechte-tokens/), [Sammlungen anlegen](/projekt/sammlungen-anlegen/).
Verifiziert am e3f92ebf 2026-10-14 (tibi-svelte-starter)
+131
View File
@@ -0,0 +1,131 @@
---
title: 'Veröffentlichen'
description: 'Vom lokalen Stack zu Staging und Live: URLs, Skripte, Reload, Token-Trennung.'
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
Lokal entwickeln, auf Staging prüfen, per Branch auf Live heben.
Der Weg: `.env`-URLs verstehen, CI-Skripte laufen lassen, Config neu laden, Köpfe sauber trennen.
Suchbegriffe: Deploy, Staging, Live, Produktion, Reload, Freischaltung.
## URL-Bild
```bash title=".env — die drei Bühnen (Platzhalter im Starter ersetzen)"
LIVE_URL=https://www # Produktion (im echten Projekt: echte Domain)
STAGING_URL=https://dev-<projekt>.staging.testversion.online
CODING_URL=https://<projekt>.code.testversion.online
```
| Bühne | Branch | Was liegt dort |
|---|---|---|
| Coding | dein Arbeits-Branch | Deine Sandbox mit Basic-Auth |
| Staging | `dev` | Prüfbühne, per `ci-staging.sh` gebaut |
| Live | `master` | Produktion, per `ci-deploy.sh` via rsync |
```text title="Branch zu Bühne: dev wird Staging, master wird Live"
push auf dev ──▶ CI: validate, build, build:server ──▶ ci-staging.sh ──▶ STAGING_URL
push auf master ─▶ CI: validate, build, build:server ──▶ ci-deploy.sh ──▶ LIVE_URL
```
Lokal startest du mit `docker-compose-local.yml` (`tibi`-Profil: fertiges Image, Projekt als `/data` gemountet).
Staging nutzt `docker-compose-staging.yml` (frisch gebauter Stack im CI-Verzeichnis).
<Aside type="caution">
Im Starter stehen Platzhalter (`__PROJECT_NAME__`, `_CUSTOMER_ID_`, `LIVE_URL=https://www`).
Erkennt `ci-deploy.sh` die Vorlage, bricht es ohne Deploy ab — erst alle Platzhalter ersetzen.
</Aside>
## Deploy-Ablauf
<Tabs>
<TabItem label="CI-Pipeline (deploy.yml)">
<Steps>
1. Checkout, Starter-Defaults auflösen, `yarn install`.
2. `yarn validate` (Svelte-Check plus TypeScript) — rot heißt Stopp.
3. `ci-modify-config.sh` passt die Config für die Zielbühne an.
4. `yarn build` (Frontend) und `yarn build:server` (SSR-Bundle nach `api/hooks/lib/app.server.js`).
5. Sourcemaps zu Sentry hochladen.
6. Branch `dev` → `ci-staging.sh`; Branch `master` → `ci-deploy.sh`.
</Steps>
</TabItem>
<TabItem label="Staging (ci-staging.sh)">
```bash title="Kurzfassung: kopieren, starten, neu laden"
rsync -av api frontend/dist frontend/assets "${API_BASEDIR}/"
docker compose -f docker-compose-staging.yml up -d --build
curl -X POST "${STAGING_URL}/api/_/admin/reload" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" -d '{}'
curl -X POST "${STAGING_URL}/api/ssr?clear=1" \
-H "Content-Type: application/json" -d '{}'
```
</TabItem>
<TabItem label="Live (ci-deploy.sh)">
```bash title="Kurzfassung: rsync, Rechte, neu laden"
rsync ... frontend/ "${RSYNC_HOST}":./frontend/ # master: ohne src und *.map
rsync ... api/ "${RSYNC_HOST}":./api/
rsync ... media "${RSYNC_HOST}":./
curl -X POST "${LIVE_URL}/api/_/admin/reload" -H "Authorization: Bearer ${ADMIN_TOKEN}" -d '{}'
curl -X POST "${LIVE_URL}/api/ssr?clear=1" -H "Content-Type: application/json" -d '{}'
```
Zugang per `RSYNC_USER`, `RSYNC_PASS`, `RSYNC_HOST`, `RSYNC_PORT` aus `.env`.
Auf `master` fliegen Quellen und Karten raus (`--exclude=src --exclude=*.map`).
</TabItem>
</Tabs>
## Reload-Endpunkte
Nach jedem Deploy: Config neu laden **und** SSR-Cache wischen — sonst siehst du alte Seiten auf neuer Config.
| Endpunkt | Kopf | Wirkung |
|---|---|---|
| `POST /api/_/admin/reload` (Live/Staging-URL) | `Authorization: Bearer <ADMIN_TOKEN>` | Projekt-Config über den Reverse-Proxy neu laden |
| `POST /api/ssr?clear=1` | keiner nötig (`public.post: true`) | Ganzen SSR-Cache löschen |
| `POST /api/v1/_/:project/_/admin/reload` | `X-Admin-Token` (Server-Ebene) | Gleicher Reload direkt am Server |
| `POST /api/v1/_/:project/_/admin/regenerate-search` | `X-Admin-Token` | Suchindex neu aufbauen ([Suche](/projekt/suche/)) |
## Token-Trennung
Vier Köpfe, vier Zwecke — aus [Rechte und Tokens](/projekt/rechte-tokens/):
| Kopf | Wert | Wofür hier |
|---|---|---|
| `Token` | `ADMIN_TOKEN` | Sammlungs-CRUD in Skripten |
| `X-Auth-Token` | JWT nach Login | Deine Nutzer-Aktionen beim Testen |
| `X-Admin-Token` | Server-Admin-Token | Server-Ebene (Projekte, Reload direkt, Backups) — **nicht** ins Deploy-Skript |
| `Authorization: Bearer …` | `ADMIN_TOKEN` | Proxy-Reload auf Live/Staging — das ist der Deploy-Kopf |
<Aside type="caution">
`ADMIN_TOKEN` legt **keine** Projekte an und ersetzt kein `X-Admin-Token`.
Im Deploy-Skript steht er nur hinter `Bearer` für den Proxy-Reload — Projekt-CRUD per JWT läuft getrennt davon.
</Aside>
## Checkliste und Abnahme
<Steps>
1. Platzhalter in `.env` ersetzt (`PROJECT_NAME`, `PRODUCTION_PATH`, `LIVE_URL`)?
2. `yarn validate`, `yarn build`, `yarn build:server` lokal grün?
3. Auf `dev` gepusht, Staging-URL geprüft (anonym, nicht nur mit Token)?
4. Auf `master` gepusht, Reload plus `?clear=1` in den CI-Logs gesehen?
5. Live-URL anonym geöffnet: Navigation da, Bilder da, kein alter Stand?
</Steps>
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Deploy bricht mit „starter template detected“ ab | Platzhalter noch drin | `.env` und `PRODUCTION_PATH` vervollständigen |
| Neue Config wirkt nicht | Reload vergessen | Reload-Endpunkt pro Bühne aufrufen |
| Alte Seite trotz neuer Daten | SSR-Cache alt | `?clear=1`, Köpfe `X-SSR-Cache`/`X-SSR-Cleared` prüfen |
| `project not found` | Namespace falsch | [Neues Projekt](/einstieg/neues-projekt/) Schritt 5 |
| 401/403 beim Reload | Falscher Kopf (z. B. `Token:` statt `Bearer`) | Tabelle oben: Deploy nimmt `Bearer`, Server-API nimmt `X-Admin-Token` |
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter, .env, ci-deploy.sh, ci-staging.sh, deploy.yml).
+199
View File
@@ -0,0 +1,199 @@
---
title: 'Felder und Widgets'
description: 'Feldtypen, Widgets, Validatoren, Choices, Fremdschlüssel, dependsOn, i18n.'
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Jedes Feld hat `name` und `type`.
Dazu kommen optional `validator` (Server-Regeln) und `meta` (Admin-Darstellung).
Beispiele unten stammen aus `content.yml`, `navigation.yml`, `medialib.yml` und `tag.yml`.
## Feldaufbau
```yaml title="content.yml — Pflichtfeld mit Pattern plus Hilfetext"
# aus content.yml — Pflichtfeld mit Pattern plus Hilfetext
- name: path
type: string
validator:
pattern: ^\/[a-z0-9\-\/]*$
required: true
meta:
label: { de: "Pfad", en: "Path" }
helperText:
de: "URL-Pfad für diese Seite, z.B. '/ueber-uns'."
en: "URL path for this page, e.g. '/about-us'."
```
- `name` passt auf `^[0-9A-Za-z_-]+$` — keine Punkte, keine Leerzeichen.
- `object` und `object[]` tragen ihre Unterfelder in `subFields`.
- Listen hängst du per `[]` an: `string[]`, `object[]`, `file[]`.
- `meta.hide: true` versteckt Technikfelder (`_pagebuilderThumbnail`, `_testdata`).
## Typ-Tabelle
Das Schema kennt elf Typen. Das Widget wählst du in `meta.widget`.
| Server-Typ | Bedeutung | Widget im Starter |
|---|---|---|
| `string` | Text, eine Zeile | Standard; `select`, `richtext`, `textarea`, `foreignKey`, `foreignMedia`, `color`-nah via `select` |
| `string[]` | Textliste | `foreignKey` mit `foreign: { collection: tag }` (`medialib.yml`: `tags`) |
| `number` | Zahl | `number` |
| `number[]` | Zahlenliste | `number` |
| `boolean` | Ja/Nein | Standard-Checkbox/`switch`; reine Flags ohne Widget (`comment.yml`: `active`) |
| `object` | Gruppe | `containerLessObject` (kein Kasten), Standard mit Kasten |
| `object[]` | Liste von Gruppen | `containerLessObjectArray`, `pagebuilder` (`content.yml`: `blocks`) |
| `file` | Upload | `file` (`medialib.yml`), `image` |
| `file[]` | Mehrere Uploads | `file` |
| `date` | Datum/Zeit | `date`, `datetime` (`content.yml`: `publication.from`) |
| `any` | Alles erlaubt | `json` |
<Aside type="note">
Der Server akzeptiert zur Laufzeit zusätzlich die Schreibweisen `bool`, `float`, `float64`, `time` und `datetime`.
In neuem YAML nutzt du trotzdem nur die elf Schema-Typen — so bleibt die Datei gegen das Schema prüfbar.
</Aside>
Alle prüfbaren Widgets aus dem Schema (25 Enum-Werte — `pagebuilder` und `pageBuilder` stehen beide drin und meinen dasselbe):
`text`, `textarea`, `number`, `checkbox`, `switch`, `select`, `selectArray`, `chipArray`, `date`, `datetime`, `file`, `image`, `json`, `richtext`, `html`, `foreignKey`, `foreignKeyChipArray`, `foreignFile`, `foreignMedia`, `pagebuilder`, `pageBuilder`, `templateProps`, `containerLessObject`, `containerLessObjectArray`, `color`.
## Validatoren
Der Server prüft `validator` bei `POST` und `PUT`.
Leere Strings bestehen `required` nicht — außer mit `allowZero: true`.
| Schlüssel | Wirkung | Beispiel aus dem Starter |
|---|---|---|
| `required: true` | Feld muss gefüllt sein | `content.yml`: `name`, `path`; `navigation.yml`: `language`, `type` |
| `pattern` | Regulärer Ausdruck | `path`: `^\/[a-z0-9\-\/]*$`; `hash`: `^[a-zA-Z0-9_-]+$` |
| `in` | Nur diese Werte | `navigation.yml`: `language` in `["de", "en"]`, `type` in `["header", "footer"]` |
| `minLength` / `maxLength` | Zeichenlänge | `elements.name`: `maxLength: 100`; `alt.de`: `maxLength: 500` |
| `min` / `max` | Zahlenbereich | — |
| `minDate` / `maxDate` | Datumsbereich | — |
| `minItems` / `maxItems` | Listenlänge | — |
| `format` | `email`, `url`, `uuid` oder `slug` | — |
| `maxFileSize` / `minFileSize` | Dateigröße (`"50MB"`) | `medialib.yml`: `file` mit `required: true`, `maxFileSize: "50MB"` |
| `accept` | Erlaubte MIME-Typen | — |
| `eval` | Eigene JavaScript-Regel | — |
| `allowZero` | Leere Werte trotz `required` erlauben | — |
## Choices und Fremdschlüssel
### Feste Auswahl
`meta.choices` listet `id` plus `name` (`de`/`en`).
Mit `widget: select` wird daraus ein Dropdown.
```yaml title="navigation.yml — feste Auswahl mit choices"
# aus navigation.yml
- name: language
type: string
validator:
in: ["de", "en"]
required: true
meta:
label: { de: "Sprache", en: "Language" }
widget: select
choices:
- id: de
name: { de: "Deutsch", en: "German" }
- id: en
name: { de: "Englisch", en: "English" }
```
Leerer `id: ""` heißt „Standard/keine Auswahl" (`containerWidth`, `padding` in `content.yml`).
### Fremdschlüssel
`meta.foreign` verweist auf eine andere Sammlung.
Der Admin zeigt dann einen Auswahl-Dialog statt einem Textfeld.
```yaml title="navigation.yml — Fremdschlüssel auf content"
# aus navigation.yml — Seite verweist auf content
- name: page
type: string
meta:
label: { de: "Seite", en: "Page" }
widget: foreignKey
foreign:
collection: content
id: id
```
| Widget | Wofür | Beispiel |
|---|---|---|
| `foreignKey` | Ein Verweis (ID als String) | `navigation.elements.page` → `content`; `comment.contentId` → `content`; `medialib.tags` → `tag` |
| `foreignMedia` | Bild aus der Mediathek | `content.yml`: `blocks.heroImage.image`, `blocks.image` → `medialib` |
| `foreignFile` / `foreignKeyChipArray` | Datei-Verweis / Mehrfach-Verweis | laut Schema verfügbar |
Auflösen musst du den Verweis beim Lesen per `lookup` — siehe [Lesen und suchen](/projekt/abfragen-lesen/).
## dependsOn: Felder ein- und ausblenden
`meta.dependsOn.eval` ist JavaScript.
`$parent` ist das umgebende Objekt — so zeigt der Pagebuilder nur passende Felder.
```yaml title="content.yml — Unterzeile nur beim Hero-Block"
# aus content.yml — Unterzeile nur beim Hero-Block
- name: subline
type: string
meta:
label: { de: "Unterzeile", en: "Subline" }
dependsOn:
eval: $parent.type == 'hero'
```
Weitere Muster aus `content.yml` zum Kopieren:
- Mehrere Typen: `$parent.type == 'features' || $parent.type == 'richtext' || $parent.type == 'accordion' || $parent.type == 'contact-form'`
- Negation: `$parent.type != ''` (Block-Ausschalter `hide`)
## i18n-Felder
Zwei Muster kommen im Starter vor:
<Tabs>
<TabItem label="Sprach-Objekt">
```yaml title="medialib.yml — Alt-Text je Sprache"
# aus medialib.yml — Alt-Text je Sprache
- name: alt
type: object
meta:
label: { de: "Alt-Text", en: "Alt Text" }
subFields:
- name: de
type: string
validator:
maxLength: 500
- name: en
type: string
validator:
maxLength: 500
```
</TabItem>
<TabItem label="Einträge koppeln">
```yaml title="content.yml — Übersetzungen gehören zusammen"
# aus content.yml (meta) — Übersetzungen gehören zusammen
meta:
i18n:
entry:
languageField: lang
groupField: translationKey
```
Jede Sprache bekommt einen eigenen Eintrag mit gleichem `translationKey`.
`path` + `lang` und `translationKey` + `lang` sind je einmalig (Unique-Index).
`lang` selbst ist ein freier String — das Schema erzwingt kein `in: [de, en]`.
</TabItem>
</Tabs>
<Aside type="caution">
`meta.protected: true` (siehe `comment.yml`: `contentId`, `author`, `message`) schützt Felder vor fremdem Überschreiben.
Setze es bei Feldern, die Nutzer nicht nachträglich ändern dürfen.
</Aside>
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter), am 4c55f82 2026-10-12 (tibi-types), am 68b42785 2026-10-15 (tibi-server, validator.go).
+234
View File
@@ -0,0 +1,234 @@
---
title: 'Hooks schreiben'
description: 'Lifecycle get/post/put/delete, Datei-Hooks, SSR- und Cache-Hooks aus dem Starter.'
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
Hooks sind JavaScript-Dateien, die der tibi-server vor oder nach einem Zugriff ausführt.
Du filterst damit öffentliche Lesezugriffe, räumst den SSR-Cache weg oder renderst Seiten.
Suche-Begriffe: Hook schreiben, Lifecycle, Cache invalidieren.
## Lifecycle: welche Stufe wann läuft
Die Stufen stehen in `tibi-types/schemas/config/hooks.schema.json`.
Jeder Hook ist `{ type: "javascript", file: "hooks/…js", timeout?: number }` — nur Datei-Hooks, kein Inline-Code.
| Methode | Stufen (in dieser Reihenfolge) |
|---|---|
| `get` | `handle`, `read`, `return`, `file` |
| `post` | `bind`, `validate`, `handle`, `create`, `return`, `bulkCreate`, `bulkReturn` |
| `put` | `bind`, `validate`, `update`, `return`, `bulkUpdate`, `bulkReturn` |
| `delete` | `delete`, `return`, `bulkDelete`, `bulkReturn` |
| `audit` | `return` |
So bindet der Starter sie ein (`api/collections/content.yml`):
```yaml title="content.yml — Hooks aus dem Starter"
# content.yml — Hooks aus dem Starter abgeschaut
hooks:
get:
read:
type: javascript
file: hooks/filter_public.js
post:
return:
type: javascript
file: hooks/clear_cache.js
put:
return:
type: javascript
file: hooks/clear_cache.js
delete:
return:
type: javascript
file: hooks/clear_cache.js
```
Die `ssr`-Sammlung nutzt eigene Stufen (`api/collections/ssr.yml`): `get.read` → `hooks/ssr/get_read.js`, `post.bind` → `hooks/ssr/post_bind.js`.
## Hook-Arten im Starter
| Datei | Stufe | Aufgabe |
|---|---|---|
| `hooks/filter_public.js` | `get.read` | Anonyme sehen nur `active:true` plus Veröffentlichungsfenster |
| `hooks/clear_cache.js` | `post/put/delete.return` | SSR-Cache nach jeder Änderung invalidieren |
| `hooks/ssr/get_read.js` | `get.read` der `ssr`-Sammlung | Seite rendern oder Cache ausliefern |
| `hooks/ssr/post_bind.js` | `post.bind` der `ssr`-Sammlung | `?clear=1` räumt den Cache, sonst Fehler `500` (Dummy-Sammlung) |
| `hooks/config.js` | kein Hook, `require`-Modul | `publishedFilter`, `ssrValidatePath()`, `apiSsrBaseURL` |
| `hooks/lib/utils.js` | kein Hook, `require`-Modul | `log()`, `clearSSRCache()` |
## Rezept: je ein get/post/put/delete-Hook mit Log
<Steps>
1. Lies den Filter-Hook: `filter_public.js` hängt den `publishedFilter` an — nur wenn `context.user.auth()` leer ist (anonym).
```js title="hooks/filter_public.js (gekürzt)"
// hooks/filter_public.js (gekürzt)
const { publishedFilter } = require("./config")
;(function () {
if (!context.user.auth()) {
/** @type {HookResponse} */
const hookResponse = {}
if (context.filter && Object.keys(context.filter).length > 0) {
hookResponse.filter = { $and: [context.filter, publishedFilter] }
} else {
hookResponse.filter = publishedFilter
}
return hookResponse
}
})()
```
Ohne `return` läuft der Hook ins Leere — eingeloggte Nutzer sehen dann alles.
2. Verstehe `clear_cache.js`: Er liest Sammlung, Methode und Entry-ID aus `context` und ruft `utils.clearSSRCache()` auf.
```js title="hooks/clear_cache.js (gekürzt)"
// hooks/clear_cache.js (gekürzt)
var utils = require("./lib/utils")
;(function () {
const col = context.collection()
const collectionName = col && col.name ? col.name : null
const req = context.request()
const method = req.method
const data = Array.isArray(context.data) ? {} : context.data || {}
let entryId = data.id || req.param("id") || null
if (!entryId) {
// DELETE-Fallback: letzte Pfad-Segmente ≥ 24 Zeichen gelten als ID
const segments = req.path.split("/")
const last = segments[segments.length - 1]
if (last && last.length >= 24) entryId = last
}
utils.clearSSRCache(collectionName, entryId, method)
})()
```
3. Merke dir die Invalidierungs-Regel aus `lib/utils.js`: `POST` löscht nur Listen-Abhängigkeiten (`col:*`), `PUT`/`DELETE` löschen Detail plus Liste (`col:id` oder `col:*`). Ohne Argumente folgt ein Komplett-Wipe.
4. Logge mit `utils.log()` (`console.log` mit `JSON.stringify`). Das ist die einzige Log-Hilfe im Starter.
</Steps>
## Wichtigste Kontexte
Vollständige Liste: [Hook-Kontexte](/reference/hook-kontexte/). Hier die fünf, die du täglich brauchst:
| Kontext | Zugriff im Starter | Wofür |
|---|---|---|
| `context.user.auth()` | `filter_public.js` | Anmeldung prüfen (`null` = anonym) |
| `context.request()` | `clear_cache.js`, `get_read.js` | Methode, Pfad, Query, Header lesen |
| `context.response.header()` | `get_read.js`, `utils.js` | `X-SSR-Cache`, `X-SSR-Cleared`, `Cache-Control` setzen |
| `context.db` | `config.js`, `get_read.js`, `utils.js` | `find`, `create`, `delete`, `deleteMany` |
| `context.tpl` / `context.http` | per [Hook-Kontexte](/reference/hook-kontexte/) | Templates rendern, externe Aufrufe |
Weitere im Starter genutzte Zugriffe: `context.filter`, `context.data`, `context.collection()`, `context.config.server().api.port`, `context.api().namespace`, `context.fs.readFile()`, `context.debug.sentryTraceId()`, `context.ssrCache` / `context.ssrDeps` (nur SSR).
## Kontext in get.read: filter, aber kein data
In `get.read` siehst du die Anfrage, nicht die Dokumente:
| Zugriff | Inhalt in get.read | Beleg |
|---|---|---|
| `context.filter` | Eingehender Mongo-Filter (Objekt oder leer) — genau das Muster aus `filter_public.js` (`context.filter && Object.keys(...)`) | Starter `filter_public.js`, Controller `collection_get.go` (hookData: filter, selector, offset, limit, sort, q, qName) |
| `context.selector`, `context.offset`, `context.limit`, `context.sort`, `context.q` | Paging, Sortierung, Suche der Anfrage | Controller `collection_get.go` |
| `context.data` | **Gibt es in get.read nicht** — erst ab `post`/`put`/`delete` (dort das Dokument) | Controller (kein `data`-Key im read-hookData) |
| `response.header()` | Erreicht den Client auch bei normalem Listen-GET (Server schreibt per Header, kein Body-Ersatz nötig) | `context_response.go` (`c.Header`) |
## Fenster-Datum: jetzt als BSON-Datum
Vergleiche gegen „jetzt" exakt wie der Starter (`api/hooks/config.js:4`):
```js title="Jetzt als Datum, Fenster inklusiv"
const now = { $date: new Date().toISOString() }
const publishedFilter = {
active: true,
$or: [
{ publication: { $exists: false } },
{ publication: null },
{
$and: [
{ $or: [{ "publication.from": { $exists: false } }, { "publication.from": null }, { "publication.from": { $lte: now } }] },
{ $or: [{ "publication.to": { $exists: false } }, { "publication.to": null }, { "publication.to": { $gte: now } }] },
],
},
],
}
```
Regeln: `new Date()` gibt es in Hooks (goja), `$lte`/`$gte` sind inklusiv, fehlende Felder brauchen die `$exists:false`/`null`-Zweige — sonst verschwinden undatierte Dokumente. Serialisierung: `{ $date: ISO-String }`.
## Rezept: Filter + 60-s-Cache + Fallback-Header
`cache.*` ist projektweit (nicht pro Request) — kombiniere ihn mit `filter`-Return und Header so:
```js title="Fensterfilter mit Cache und Fallback-Header"
;(function () {
if (context.user.auth()) return // nur anonym filtern
const key = "content:published" // Key-Konvention: Sammlung:Zweck
const hit = context.cache.get(key)
if (hit) {
if (context.filter && Object.keys(context.filter).length > 0) {
return { filter: { $and: [context.filter, hit] } }
}
return { filter: hit }
}
context.response.header("X-Publication-Fallback", "uncached")
const filter = { active: true } // Kurzform; Fenster siehe oben
context.cache.set(key, filter, 60000) // 60 s = 60000 ms
if (context.filter && Object.keys(context.filter).length > 0) {
return { filter: { $and: [context.filter, filter] } }
}
return { filter: filter }
})()
```
Key-Regel: Namen mit `Sammlung:Zweck` vergeben — der Cache teilt sich alle Hooks des Projekts.
## SSR-Hooks: Rendern und Cachen
```text title="SSR-Ablauf im Überblick"
GET ssr?url=/de/… → get_read.js → ssrValidatePath(url) → Cache-Treffer? → app.server rendern → in ssr-Sammlung speichern → HTML werfen
```
- `ssrValidatePath()` aus `config.js`: `/` und `/de|/en` rendern, Sprachpräfix vor der `content`-Abfrage abstreifen, kanonische URL oder `-1` (404) / `0` (kein SSR) zurückgeben.
- `get_read.js` speichert `path`, `content`, `validUntil`, `dependencies` (`content:<id>`, `content:*`). Parallele Schreiber mit gleichem `path` verlieren per Unique-Index — der Fehler wird geschluckt.
- `context.is404 = true` (gesetzt von `NotFound.svelte`) liefert HTTP `404` ohne zu cachen.
- `post_bind.js`: nur `?clear=1` ruft `clearSSRCache()` ohne Argumente (Komplett-Wipe) und wirft `{ status: 200, removed }`. Jeder andere POST wirft `500` — `ssr` ist eine Dummy-Sammlung.
## Fehlerbehandlung
<Tabs>
<TabItem label="Antwort werfen">
```js title="Antwort direkt werfen"
throw { status: 403, msg: "invalid auth" }
throw { status: 200, log: false, html: tpl }
```
So antwortet `get_read.js`: HTML oder Fehler direkt werfen, kein `return`.
Der Throw-Umschlag (`internal/models/hook.go`): `status` (Zahl, Vorgabe `500`), `msg` (Text, wird an den JS-Fehler angehängt), `json` (Antwortkörper — ohne `json`-Feld antwortet der Server das geworfene Objekt selbst), `html`/`string`/`bytes`/`file` (direkte Antwort statt JSON). **Achtung:** Für den Fehler*text* zählt nur `msg` — ein Feld `message` landet zwar im Body (wenn du kein `json` setzt), ändert aber nicht den Fehlertext. Schreibe deshalb `msg`.
</TabItem>
<TabItem label="Filter zurückgeben">
```js title="Filter zurückgeben"
return hookResponse // { filter: { $and: [...] } }
```
Nur `get.read`-Hooks wie `filter_public.js` geben ein Objekt mit `filter` zurück.
</TabItem>
</Tabs>
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Öffentliche Liste zeigt Entwürfe | `filter_public.js` fehlt oder gibt nichts zurück | `get.read`-Hook in der YAML prüfen |
| SSR zeigt alte Seite nach PUT | `clear_cache.js` fehlt an `put.return` | Hook-Zeile aus `content.yml` übernehmen |
| `ssr`-POST liefert `500` | Normal — Dummy-Sammlung ohne `?clear=1` | `?clear=1` anhängen oder lassen |
| DELETE räumt nichts weg | ID weder in `data.id` noch im Pfad | Pfad-Segment ≥ 24 Zeichen prüfen |
Weiter: [Lesen und suchen](/projekt/abfragen-lesen/), [Rechte und Tokens](/projekt/rechte-tokens/), [Hook-Kontexte](/reference/hook-kontexte/).
Verifiziert am e3f92ebf 2026-10-14 (tibi-svelte-starter), am 68b42785 2026-10-15 (tibi-server), am 4c55f824 2026-10-12 (tibi-types)
+144
View File
@@ -0,0 +1,144 @@
---
title: 'Jobs und Aktionen'
description: 'Cron-Jobs und Action-Endpunkte aus Projektsicht anlegen und beobachten.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid } from '@astrojs/starlight/components';
Jobs laufen nach Zeitplan (Cron), Aktionen auf Abruf per HTTP.
Beides sind JavaScript-Programme in der goja-Laufzeit des **tibi-server** — wie Hooks, aber ohne dass du sie erst an ein Formular hängen musst.
Suchbegriffe: Cron-Job, geplante Aufgabe, Action-Endpunkt, Kontaktformular-Backend.
<CardGrid>
<Card title="Jobs">Laufen nach Zeitplan (Cron) — zum Beispiel Aufräumen alle 5 Minuten.</Card>
<Card title="Aktionen">Laufen auf Abruf per HTTP — zum Beispiel Kontaktformular oder Webhook.</Card>
</CardGrid>
<Aside type="note">
Der Starter liefert **keine** `jobs:`- oder `actions:`-Abschnitte in `api/config.yml` mit.
Du legst sie selbst an — die Schlüssel `jobs` und `actions` gehören zum Projektschema, die Dateiformate zu `job.schema.json` und `action.schema.json`.
</Aside>
## Job anlegen
```yaml title="api/config.yml — Job registrieren"
jobs:
- cron: "*/5 * * * *" # alle 5 Minuten (klassisches 5-Felder-Format)
type: javascript
file: jobs/cleanup.js
timeout: 30000 # Millisekunden, optional
```
| Feld | Pflicht | Bedeutung |
|---|---|---|
| `cron` | nein (Schema-Pflicht: nur `type` + `file`) — aber ohne `cron` läuft der Job nie | 5 Felder (`Minute Stunde Tag Monat Wochentag`), z. B. `0 8 * * *` für täglich 8 Uhr |
| `type` | ja | Immer `javascript` |
| `file` | ja | Pfad zum Skript, relativ zum Projekt |
| `timeout` | nein | Abbruch nach N Millisekunden — setze ihn immer, damit ein Hänger nicht ewig läuft |
<Steps>
1. Skript unter `jobs/` ablegen (gleiche Hook-Laufzeit, aber `JOB_CONTEXT`: kein HTTP-Request).
2. `jobs:`-Block in `api/config.yml` ergänzen.
3. Config neu laden (`POST …/_/admin/reload`, siehe [Veröffentlichen](/projekt/deploy/)).
4. Zum Testen erst Minutentakt (`* * * * *`), Log prüfen, dann auf den echten Takt stellen.
</Steps>
```js title="jobs/cleanup.js — Beispiel mit Timeout im Blick"
// Läuft unter der Projekt-UID (suexec-isoliert), fast alle Hook-Pakete verfügbar.
// Halte jeden Durchlauf kurz: Lieber oft und klein als selten und groß,
// damit der Timeout nie zuschlägt.
```
## Action anlegen
Aktionen sind Hook-basierte Endpunkte **ohne** Sammlung dahinter — ideal für Kontaktformulare, Newsletter-Anmeldungen oder Webhooks.
Sie nutzen dasselbe Rechte-, Hook-, CORS- und Audit-System wie Sammlungen, haben aber keine Felder, Indizes, Uploads oder Suche.
```yaml title="api/config.yml — Actions einbinden"
actions:
- !include actions/contact-form.yml
```
```yaml title="api/actions/contact-form.yml — eine Action"
name: contact-form
meta:
label: { de: "Kontaktformular", en: "Contact Form" }
permissions:
public:
methods:
post: true # anonym senden darf jeder
user:
methods:
post: true
get: true
hooks:
post:
bind:
type: javascript
file: hooks/contact-form/post_bind.js
validate:
type: javascript
file: hooks/contact-form/post_validate.js
handle:
type: javascript
file: hooks/contact-form/post_handle.js
return:
type: javascript
file: hooks/contact-form/post_return.js
```
| Baustein | Regeln |
|---|---|
| `name` | Nur `[0-9A-Za-z_-]`, erreichbar unter `/_actions/<name>` |
| `permissions` | Gleiche Blöcke wie Sammlungen (`public`, `user`, `token:…`) — siehe [Rechte und Tokens](/projekt/rechte-tokens/) |
| `hooks` | Ein String (Kurzform) oder Stufen `bind`/`validate`/`handle`/`return`, je `get`/`post` |
| `cors` | Eigener Eintrag mit `merge: true/false` (anhängen oder ersetzen) — die [CORS-Kaskade](/server/sicherheit/) beginnt bei `/_actions/<name>` |
| `audit` | `enabled: true` plus `actions: [get, post]` protokolliert Aufrufe |
<Tabs>
<TabItem label="Per API auslösen">
```bash title="Action per curl senden"
curl -X POST http://localhost:8080/api/v1/_/<namespace>/_actions/contact-form \
-H "Content-Type: application/json" \
-d '{"name":"Aylin","email":"a@example.de","message":"Hallo!"}'
```
</TabItem>
<TabItem label="Im Admin auslösen">
Rufe die Action-Übersicht im **tibi-admin** auf: Karten pro Action, Methoden-Tabs (`get`/`post`), Eingabemaske bei ausführbaren Methoden, Ergebnis- oder Fortschrittsanzeige.
Gesperrte Methoden zeigen den Grund als Hinweis statt eines Buttons.
</TabItem>
</Tabs>
## MCP-Werkzeuge für Aktionen
Zwei MCP-Werkzeuge spiegeln diese Seite: `actions_list` (Aktionen auflisten, braucht Lese-Recht) und `action_call` (Aktion aufrufen, `method: get/post`, Scope folgt der Methode). Signaturen, Beispiele und Fehler stehen in [MCP nutzen](/projekt/mcp-nutzen/) — dort ist die einzige Referenzstelle.
## Beobachten
| Signal | Wo |
|---|---|
| Job-Fehler | Server-Log (`docker compose logs -f tibiserver`) mit Job-Name und Ursache |
| Action-Antwort | HTTP-Status plus Hook-`return`-Payload; Fortschritt bei langen Läufen |
| Nutzung und Quotas | LLM-Nutzungsablage und `/metrics` (siehe [Betrieb](/server/betrieb/)) |
| Aufruf-Protokoll | Audit-Log, wenn `audit.enabled` auf Action-Ebene steht (siehe [Sicherung](/projekt/audit-backup/)) |
## Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Job bricht kommentarlos ab | `timeout` gerissen oder Cron-Feld ungültig | Timeout erhöhen, Cron auf 5 Felder prüfen, Server-Log lesen |
| `permission denied` auf `/_actions/…` | Falsche Rolle oder falscher Kopf | `permissions`-Block und Token-Kopf prüfen ([Rechte und Tokens](/projekt/rechte-tokens/)) |
| Browser blockt die Action | CORS-Eintrag der Action fehlt | `cors:` an der Action setzen, `merge`-Verhalten beachten |
| Action läuft, schreibt aber nichts | `validate`/`handle`-Hook fehlt oder wirft | Hook-Kette Stufe für Stufe testen |
| `project not found` | Namespace falsch | [Neues Projekt](/einstieg/neues-projekt/) Schritt 5 |
Verifiziert am 4c55f82 2026-10-12 (tibi-types, job- und action-schema), am 68b42785 2026-10-15 (tibi-server, jobs- und actions-docs), am 2b091d89 2026-10-13 (tibi-admin-nova, actions-test-plan 40).
+172
View File
@@ -0,0 +1,172 @@
---
title: 'LLM nutzen'
description: 'Chat, Anbieter, Budgets und Streaming aus Projekt-Sicht: was der Server kann und was es kostet.'
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Der **tibi-server** chattet für dein Projekt: Zusammenfassen, Entwürfe, eigene Features hinter deinem Backend.
Abgrenzung: Diese Seite beschreibt die **API**.
Die Chat-Oberfläche im Admin (Knopf, Verlauf, Stimme, Werkzeuge) steht unter [KI-Funktionen](/admin/ki-funktionen/) — dort läuft alles über den Proxy, hier rufst du Provider direkt.
## Chat aufrufen
Zwei Routen (`setup.go`): `POST /api/v1/llm/chat` chattet, `GET /api/v1/llm/providers` listet Anbieter (ohne Secrets).
Anmeldung wie bei REST (siehe [Rechte und Tokens](/projekt/rechte-tokens/)); dein PAT braucht den LLM-Scope.
<Tabs>
<TabItem label="Normal">
```bash title="POST /llm/chat — normal"
curl -s -X POST http://localhost:8080/api/v1/llm/chat \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"provider":"<anbieter>","model":"<modell>",
"messages":[{"role":"user","content":"Fasse diesen Absatz zusammen: …"}]}'
# → {"id":"…","model":"<modell>","choices":[{"index":0,
# "message":{"role":"assistant","content":"…"},"finishReason":"stop"}],
# "usage":{"promptTokens":12,"completionTokens":30,"totalTokens":42}}
```
Pflichtfelder (`llm_chat.go`): `provider` und `messages`.
`model` ist optional — dann gilt das Default-Modell (nächster Abschnitt).
</TabItem>
<TabItem label="Streaming">
```bash title="POST /llm/chat — Streaming (stream:true)"
curl -N -X POST http://localhost:8080/api/v1/llm/chat \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"provider":"<anbieter>","messages":[{"role":"user","content":"Hallo"}],
"stream":true}'
# → data: {"id":"…","model":"…","choices":[{"index":0,"delta":{"role":"assistant","content":"Hal"}}]}
# → data: {"id":"…","choices":[{"index":0,"delta":{"content":"lo"}}]}
# → data: [DONE]
```
Format: `Content-Type: text/event-stream`, Chunks als `data: <JSON>`, Ende als `data: [DONE]`.
Fehler im Stream kommen als `data: {"error":"…"}`.
</TabItem>
</Tabs>
Optionale Felder (`models.go`, `ChatRequest`): `temperature`, `maxTokens`, `stream`.
Zugriff: Dein PAT braucht den Scope `llm.chat` (`config/scope.go`, `ScopeLLMChat`).
Fehlt Anmeldung oder Scope, antwortet der Handler `403` — noch vor jeder Provider- oder Budget-Prüfung (`llm_chat.go`: `GetAuth` + `EnforceScope`).
## Anbieter und Modelle
Anbieter stehen in der Server-`config.yml` (`llm.providers`, Typ `openai` oder `openai-compatible`).
Namen, `defaultModel`, `models`-Allowlist und `maxTokensPerRequest` legt der Betrieb fest — keine erfundenen Namen verwenden.
```bash title="GET /llm/providers — was ist konfiguriert"
curl -s http://localhost:8080/api/v1/llm/providers \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN'
# → [{"name":"<anbieter>","type":"openai-compatible",
# "defaultModel":"<modell>","models":["<modell>", …]}]
```
Modell-Auflösung (`openai.go`, `resolveModel`):
| Anfrage | Ergebnis |
|---|---|
| `model` gesetzt und in `models` | Genau dieses Modell |
| `model` leer | `defaultModel` des Providers |
| Weder noch | `400 no model specified and no default model configured` |
| `model` nicht in der Allowlist | `400 model … is not allowed, available models: …` |
| `models`-Allowlist leer | Keine Prüfung — jedes Modell passiert (`len(p.models) > 0` schaltet die Prüfung erst ein) |
Provider-Mechanik (`registry.go`, `openai.go`, `server.go`):
- Registriert wird nur Typ `openai` oder `openai-compatible` — andere Typen überspringt `NewRegistry` stillschweigend (kein Fehler, der Anbieter fehlt danach einfach).
- Unbekannter Name → `400 LLM provider "…" not found` (`registry.go`, `Get`).
- Leere `baseURL` fällt auf `https://api.openai.com/v1` zurück; der API-Schlüssel wandert server-seitig in den `Authorization: Bearer`-Header — `GET /llm/providers` liefert ihn nie aus (`apiKey` hat `json:"-"`, `setup.go` baut bewusst nur `name`, `type`, `defaultModel`, `models` in die Antwort).
- `maxTokensPerRequest` (> 0) deckelt still: Fehlt `maxTokens` oder liegt es darüber, sendet der Server den konfigurierten Wert an den Provider (`buildRequest`). Das schützt vor Riesen-Antworten, steht aber nicht in der API-Antwort — plane deine Budgets mit dem konfigurierten Wert, nicht mit deinem Wunschwert.
<Aside type="note">
Preise stehen nirgends im Code — erfinde keine.
Kosten steuerst du über Budgets (nächster Abschnitt), nicht über Modellnamen.
</Aside>
## Budgets
Jeder Aufruf kostet Token aus deinem Budget — sonst antwortet der Server `429`.
Budget-Typ (`models/user.go`, `LLMBudget`):
| Feld | Bedeutung |
|---|---|
| `provider` | Für welchen Anbieter das Budget gilt |
| `tokenBudget` | Gesamtk approbierte Token (`0` = unbegrenzt auf dieser Ebene) |
| `tokensUsed` | Bereits verbrauchte Token (wächst pro Aufruf) |
| `periodStart` / `periodEnd` | Zeitraum; nach `periodEnd` ist Schluss |
Regeln (`quota.go`, dazu Reihenfolge in `llm_chat.go`):
- **Benutzer-Budget ist Pflicht.** Kein Eintrag für den Provider → `429 no LLM budget configured for provider "…"`.
- `tokensUsed >= tokenBudget` → `429 LLM token budget exhausted for provider "…" (used X / Y)`.
- `periodEnd` überschritten → `429 LLM budget period expired for provider "…"`.
- **Org-Budget ist optional** (`CheckBudgetIfSet`): Nur wenn deine Org einen Eintrag hat, zählt er zusätzlich. Die Org wird nur gelesen, wenn dein Konto eine `primaryOrgId` hat — sonst entfällt die Prüfung komplett. Org-Fehltexte tragen ein `org `-Präfix (`org LLM token budget exhausted …`, `org LLM budget period expired …`), damit du in der `429` erkennst, welche Ebene blockiert.
- `tokenBudget: 0` heißt auf dieser Ebene unbegrenzt — ersetzt aber **nicht** den Pflicht-Eintrag auf Benutzerebene.
- Prüfreihenfolge pro Aufruf: erst Benutzer-Budget (`CheckBudget`), dann — nur bei gesetzter `primaryOrgId` — Org-Budget (`CheckBudgetIfSet`). Erst danach geht die Anfrage an den Provider.
- Jeder Aufruf schreibt `tokensUsed` bei Benutzer **und** Org fort (jeweils `$set` auf `llmBudgets`) plus eine Zeile in `llm_usage` (`usage_model.go`, Collection `llm_usage`): Benutzer (`userId`), Org (`orgId`), Provider, Modell, Prompt-/Completion-/Gesamt-Token, Zeit (`timestamp`). Schlägt das Buchen fehl, merkst du nichts: Beide Handler ignorieren den Fehler (`_ = qm.RecordUsage(…)`), die Antwort bleibt gültig.
Token-Schätzung, wenn der Provider keine `usage` liefert: ca. 4 Zeichen ≈ 1 Token, plus 4 Token Rahmen pro Nachricht (`EstimateUsage`).
Geschätzte Werte tragen `estimated: true`.
`EstimateUsage` im Detail (`models.go`):
- `EstimateTokens(text)` = `len(text) / 4` (Byte-Länge, Integer-Division), mindestens 1 bei nicht-leerem Text.
- Pro Nachricht: `EstimateTokens(role) + EstimateTokens(content) + 4` (Rahmen).
- Completion: `EstimateTokens(Antworttext)`; `totalTokens` ist die Summe, `estimated` immer `true`.
- Anwendung: Nicht-Streaming übernimmt die Schätzung nur, wenn `usage.totalTokens == 0`. Im Stream hängt die `usage` am letzten Chunk (der Provider sendet sie wegen `stream_options: {include_usage: true}` mit); fehlt sie, schätzt der Server aus deinen Nachrichten plus dem akkumulierten Stream-Text.
<Tabs>
<TabItem label="Normal">
Fehlerfall normal — Budget aufgebraucht:
```bash title="POST /llm/chat — 429 bei leerem Budget"
curl -s -X POST http://localhost:8080/api/v1/llm/chat \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"provider":"<anbieter>","messages":[{"role":"user","content":"Hallo"}]}'
# → HTTP 429 {"error":"LLM token budget exhausted for provider \"<anbieter>\" (used 10000 / 10000)"}
```
</TabItem>
<TabItem label="Streaming">
Fehlerfall Streaming — Abbruch mitten im Stream:
```bash title="POST /llm/chat — Fehler im Stream"
curl -N -X POST http://localhost:8080/api/v1/llm/chat \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"provider":"<anbieter>","messages":[{"role":"user","content":"Hallo"}],
"stream":true}'
# → data: {"id":"…","model":"…","choices":[{"index":0,"delta":{"role":"assistant","content":"Hal"}}]}
# → data: {"error":"…"} # statt weiterer Chunks + [DONE]
```
</TabItem>
</Tabs>
Dazu die zweite Fehlerlage: Scheitert schon der Stream-Aufbau (Provider down, Modell falsch), kommt gar kein SSE — sondern direkt `502` (Text nur, wenn der Betreiber keine generische Fehlermeldung gesetzt hat: `response.error.message` in der Server-Config ersetzt den Body durch einen festen Text). Das `[DONE]` gibt es nur nach vollständigem Lesen (`io.EOF`).
## Fehler
| Antwort | Ursache | Lösung |
|---|---|---|
| `403` | Keine Anmeldung oder LLM-Scope fehlt | Token und [Rechte](/projekt/rechte-tokens/) prüfen |
| `400 provider is required` / `messages are required` | Pflichtfeld fehlt | Beide Felder senden |
| `400 provider … not found` | Anbietername falsch | `GET /llm/providers` lesen, Namen kopieren |
| `400 model … is not allowed, available models: …` | Modell nicht freigegeben | Modell aus der Allowlist nehmen oder leer lassen |
| `400 LLM provider "…" not found` | Anbietername falsch oder Typ nicht registriert | `GET /llm/providers` lesen, Namen kopieren |
| `400 no model specified and no default model configured` | Weder `model` noch `defaultModel` gesetzt | `model` mitsenden oder Betrieb um `defaultModel` bitten |
| `429` Budget | Kein Budget, aufgebraucht oder Zeitraum abgelaufen (`org `-Präfix = Org-Ebene) | Budget prüfen / beim Betrieb anfragen |
| `502` | Provider antwortet nicht oder Stream bricht ab | Später erneut versuchen, `maxTokens` senken |
| `413` | Body zu groß | Nachrichten kürzen |
Budget-Verwaltung (wer darf was): `llmBudgets` stehen am Benutzer (`models/user.go`) und an der Org. Nur System-Admins ändern sie frei — Org-Admins verlieren das Feld still (`llmBudgets` wird auf `nil` gesetzt bzw. aus Änderungen gestrichen), Team-Admins und Mitglieder bekommen beim Schreibversuch `updating llmBudgets only allowed for admins`. Wende dich also für Budget-Erhöhungen an den Betrieb.
Verifiziert am 68b42785 2026-10-16 (tibi-server: setup.go LLM-Routen + Providers-Antwort ohne Secrets, llm_chat.go inkl. 429/502/413-Mapping und RecordUsage, registry.go Typ-Filter, provider.go, models.go EstimateUsage, openai.go resolveModel + buildRequest-Clamp + include_usage, quota.go CheckBudget/CheckBudgetIfSet/RecordUsage, usage_model.go llm_usage, user.go LLMBudget + Feldschutz, config/scope.go llm.chat, server.go ConfigLLMProvider).
+754
View File
@@ -0,0 +1,754 @@
---
title: 'MCP nutzen'
description: 'KI-Werkzeugzugriff auf Tibi: Transport, Anmeldung, alle Werkzeuge und die Agenten-Anleitung.'
---
import { Aside, Tabs, TabItem, Steps, Badge } from '@astrojs/starlight/components';
MCP gibt KI-Agenten Werkzeugzugriff auf Tibi — statt Klicks ruft der Agent `tools/list` und `tools/call` auf.
Transport: Streamable-HTTP mit JSON-RPC 2.0.
Route: fest `GET` + `POST /api/v1/mcp` (immer registriert; `mcp.route` wird ignoriert — siehe [Schnittstellen](/server/schnittstellen/)).
## Anmeldung
Nur PAT via `X-Auth-Token` (`tibi_pat_…`).
JWT, Admin-Token und anonyme Aufrufe scheitern — auch mit vollen Rechten.
Jedes Werkzeug prüft zusätzlich Scope plus Live-Rechte des Token-Besitzers (siehe [Rechte und Tokens](/projekt/rechte-tokens/), [Token-Köpfe](/reference/token-koepfe/)).
<Tabs>
<TabItem label="Handshake">
```bash title="initialize — Handshake"
curl -s -X POST http://localhost:8080/api/v1/mcp \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# → {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05",
# "serverInfo":{"name":"tibi-server-mcp","version":"v1"},
# "capabilities":{"tools":{}}}}
```
</TabItem>
<TabItem label="tools/list + tools/call">
```bash title="tools/list — alle Werkzeuge"
curl -s -X POST http://localhost:8080/api/v1/mcp \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
```
```bash title="tools/call — Einträge lesen"
curl -s -X POST http://localhost:8080/api/v1/mcp \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: tibi_pat_DEIN_TOKEN' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"documents_list",
"arguments":{"namespace":"<ns>","collection":"content","limit":5}}}'
# → {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"…JSON…"}]}}
```
`GET /api/v1/mcp` ist nur ein Probeaufruf (Protokoll, Version, Werkzeugliste, kein JSON-RPC-Body).
</TabItem>
</Tabs>
## Alle Werkzeuge (36)
Gehäuse: `internal/mcp/tools_crud.go` (10) + `tools_discovery.go` (12) + `tools_system.go` (14).
Jede Antwort ist ein JSON-RPC-Envelope mit der Request-`id`.
| Werkzeug | Wofür | Scope |
|---|---|---|
| `documents_list` | Einträge listen (Filter, Sort, Limit wie [Lesen und suchen](/projekt/abfragen-lesen/)) | `namespace.<ns>.<col>.get` |
| `document_get` | Ein Eintrag per ID | `.get` |
| `documents_count` | Nur Anzahl | `.get` |
| `documents_facet` | Werte je Feld zählen | `.get` |
| `queue_status` | Suchindex-Warteschlange | Projekt-Leserecht |
| `document_create` | Eintrag anlegen | `.post` |
| `document_update` | Eintrag ändern | `.put` |
| `document_delete` | Eintrag löschen | `.delete` |
| `file_read` | <Badge text="REST-only" variant="caution" /> Datei lesen: `GET /_/:project/:collection/:id/:field/*path` | `.get` |
| `action_call` | Aktion aufrufen (`method: get/post`) | Aktions-Scope |
| `projects_list` / `project_get` | Projekte auflisten / ein Projekt | `project.read` |
| `collections_list` | Sammlungen eines Projekts | `project.read` |
| `collection_describe` | Schema + `meta` einer Sammlung | `project.read` |
| `actions_list` / `action_describe` | Aktionen auflisten / beschreiben | `project.read` |
| `jobs_list` | Jobs auflisten | Projekt-Leserecht |
| `openapi_get` | OpenAPI-Ausschnitt | `project.read` |
| `system_meta_get` | System-Metadaten | `project.read` |
| `agent_guide_get` | Agent-Anleitung (`meta.agent`) lesen | `project.read` |
| `assets_list` / `asset_read` | Projekt-Assets listen / <Badge text="REST-only" variant="caution" /> lesen per `GET /_/:project/_/assets/:name/*path` | `project.read` |
| `users_list` / `user_get` | Benutzer listen / lesen | `user.read` |
| `user_create` / `user_update` / `user_delete` | <Badge text="REST-only" variant="caution" /> (siehe unten) | `user.create` / `.update` / `.delete` |
| `orgs_list` / `org_get` | Orgs listen / lesen | `org.read` |
| `teams_list` / `team_get` | Teams listen / lesen | `team.read` |
| `audit_query` | Audit-Log abfragen | `audit.read` + Sammlungs-Leserecht |
| `project_reload` | <Badge text="REST-only" variant="caution" /> | `project.reload` |
| `regenerate_search` | <Badge text="REST-only" variant="caution" /> | `project.regenerate-search` |
| `backup_create` | <Badge text="REST-only" variant="caution" /> | `project.backup` |
| `snapshots_list` | <Badge text="REST-only" variant="caution" /> | `project.snapshot` |
Methoden-Mapping (`method.go`) ist sicherheitsrelevant: Lese-Werkzeuge laufen als `GET`, `document_create` als `POST`, `document_update` als `PUT`, `document_delete` als `DELETE`.
So greifen deine [Hooks](/projekt/hooks-schreiben/) (Stufen je Methode, Kontexte in [Hook-Kontexte](/reference/hook-kontexte/)) genau wie bei REST.
## Werkzeug-Referenz (alle 36)
Pro Werkzeug: Signatur (Pflichtparameter mit `*`, Stand `tools_crud.go`, `tools_discovery.go`, `tools_system.go`), genau ein `tools/call`-Beispiel und ein Fehlerfall aus dem Code (`mcp_tools_impl.go`, `guards.go`, `query.go`, `method.go`).
Ohne PAT scheitert jedes Werkzeug mit 403 (`mcp requires a personal access token (X-Auth-Token: tibi_pat_...)`, `guards.go`); ein Tippfehler im Namen gibt `-32601 unknown tool` (`mcp_handler.go`).
### documents_list
Signatur: `namespace*`, `collection*`, optional `filter`, `sort`, `limit`, `offset`, `select`, `projection`, `lookup`, `aggregate`, `q`, `qName`, `count`, `extra`. Scope: `namespace.<ns>.<col>.read` (bei `lookup`/`aggregate` zusätzlich Lese-Recht auf das Ziel). Methode: `GET`.
```json title="documents_list — tools/call"
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "documents_list",
"arguments": {"namespace": "<ns>", "collection": "content", "limit": 5}
}
}
```
Fehler: ohne `namespace`/`collection` → 400 (`namespace and collection are required`); lange Listen kappt `maxResultItems` (Standard 100).
### document_get
Signatur: `namespace*`, `collection*`, `id*`, optional `select`, `lookup`, `extra`. Scope: `namespace.<ns>.<col>.read`. Methode: `GET`.
```json title="document_get — tools/call"
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "document_get",
"arguments": {"namespace": "<ns>", "collection": "content", "id": "<id>"}
}
}
```
Fehler: unbekannte `id` → 404 (`not found`); unlesbare Hex-ID → 400.
### documents_count
Signatur: `namespace*`, `collection*`, optional `filter`, `q`, `extra`. Scope: `namespace.<ns>.<col>.read`. Methode: `GET`.
```json title="documents_count — tools/call"
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "documents_count",
"arguments": {"namespace": "<ns>", "collection": "content"}
}
}
```
Fehler: ohne Leserecht am Ziel → 403 (`permission denied` bzw. Scope-Fehler).
### documents_facet
Signatur: `namespace*`, `collection*`, `fields*`, optional `filter`, `extra`. Scope: `namespace.<ns>.<col>.read`. Methode: `GET`.
```json title="documents_facet — tools/call"
{
"jsonrpc": "2.0",
"id": 13,
"method": "tools/call",
"params": {
"name": "documents_facet",
"arguments": {"namespace": "<ns>", "collection": "content", "fields": "status"}
}
}
```
Fehler: ohne `fields` → 400 (`missing required parameter: facet` auf Controller-Ebene).
### queue_status
Signatur: `namespace*`, `collection*`. Scope: `namespace.<ns>.<col>.read`. Methode: `GET`.
```json title="queue_status — tools/call"
{
"jsonrpc": "2.0",
"id": 14,
"method": "tools/call",
"params": {
"name": "queue_status",
"arguments": {"namespace": "<ns>", "collection": "content"}
}
}
```
Fehler: unbekannte Sammlung → 404 auf Controller-Ebene.
### document_create
Signatur: `namespace*`, `collection*`, `data*`, optional `items[]` (Bulk). Datei-Felder als `{src: "data:<mime>;base64,…", path: "name.jpg"}` hochladen (Typ wird serverseitig erschnüffelt). Scope: `namespace.<ns>.<col>.create`. Methode: `POST`.
```json title="document_create — tools/call"
{
"jsonrpc": "2.0",
"id": 15,
"method": "tools/call",
"params": {
"name": "document_create",
"arguments": {"namespace": "<ns>", "collection": "content", "data": {"name": "Neu"}}
}
}
```
Fehler: ohne Schreibrecht → 403; Pflichtfeld-Verletzung → 400 aus der Validierung.
### document_update
Signatur: `namespace*`, `collection*`, dazu `id` (einzeln) oder `filter` (Bulk), plus `data`. Gespeicherte Datei-`src` zurückgeben behält die Datei. Scope: `namespace.<ns>.<col>.update` (+ Bulk-Freigabe). Methode: `PUT`.
```json title="document_update — tools/call"
{
"jsonrpc": "2.0",
"id": 16,
"method": "tools/call",
"params": {
"name": "document_update",
"arguments": {"namespace": "<ns>", "collection": "content", "id": "<id>", "data": {"name": "Neu"}}
}
}
```
Fehler: unbekannte `id` → 404; Bulk ohne `filter` → 400 (`filter query parameter is required`).
### document_delete
Signatur: `namespace*`, `collection*`, dazu `id` (einzeln) oder `filter` (Bulk). Scope: `namespace.<ns>.<col>.delete` (+ Bulk-Freigabe). Methode: `DELETE`.
```json title="document_delete — tools/call"
{
"jsonrpc": "2.0",
"id": 17,
"method": "tools/call",
"params": {
"name": "document_delete",
"arguments": {"namespace": "<ns>", "collection": "content", "id": "<id>"}
}
}
```
Fehler: ohne Löschrecht → 403; Bulk ohne `filter` → 400 (`filter query parameter is required for bulk delete`).
### file_read
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `namespace*`, `collection*`, `id*`, `field*`, optional `path`. Scope: `namespace.<ns>.<col>.read`.
```json title="file_read — tools/call"
{
"jsonrpc": "2.0",
"id": 18,
"method": "tools/call",
"params": {
"name": "file_read",
"arguments": {"namespace": "<ns>", "collection": "content", "id": "<id>", "field": "image"}
}
}
```
Fehler: immer 400 (`use GET file endpoint (public like web)`) — nutze stattdessen `GET /_/:project/:collection/:id/:field/*path`.
### action_call
Signatur: `namespace*`, `action*`, optional `method` (`get` oder `post`, Standard `post`), `data`. Scope: `namespace.<ns>._actions.<name>.read` bzw. `.create`. Methode: `GET` oder `POST` je nach `method`.
```json title="action_call — tools/call"
{
"jsonrpc": "2.0",
"id": 19,
"method": "tools/call",
"params": {
"name": "action_call",
"arguments": {"namespace": "<ns>", "action": "versand", "method": "post", "data": {}}
}
}
```
Fehler: `method: "put"` → 400 (`action method must be get or post`, `method.go`).
### projects_list
Signatur: optional `limit`, `offset`. Scope: `project.read`. Methode: `GET`.
```json title="projects_list — tools/call"
{
"jsonrpc": "2.0",
"id": 20,
"method": "tools/call",
"params": {"name": "projects_list", "arguments": {"limit": 10}}
}
```
Fehler: ohne `project.read`-Scope → 403; fremde Orgs filtert die Sichtbarkeit still heraus (leere Liste trotz Daten).
### project_get
Signatur: `id*`. Scope: `project.read`. Methode: `GET`.
```json title="project_get — tools/call"
{
"jsonrpc": "2.0",
"id": 21,
"method": "tools/call",
"params": {"name": "project_get", "arguments": {"id": "<id>"}}
}
```
Fehler: ohne `id` → 400 (`id is required`); unsichtbar/ unbekannt → 404 (`not found`).
### collections_list
Signatur: `namespace*`. Scope: `project.read` plus Lese-Recht je Sammlung. Methode: `GET`.
```json title="collections_list — tools/call"
{
"jsonrpc": "2.0",
"id": 22,
"method": "tools/call",
"params": {"name": "collections_list", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: ohne `namespace` → 400 (`namespace is required`); unbekanntes Projekt → 404 (`project not found`).
### collection_describe
Signatur: `namespace*`, `collection*`, optional `lang`. Gibt Schema, `agent`- und `query`-Meta (bereinigt, ohne Secrets) plus `yourPermissions` zurück. Scope: `project.read` plus Lese-Recht auf die Sammlung. Methode: `GET`.
```json title="collection_describe — tools/call"
{
"jsonrpc": "2.0",
"id": 23,
"method": "tools/call",
"params": {
"name": "collection_describe",
"arguments": {"namespace": "<ns>", "collection": "content"}
}
}
```
Fehler: unbekannte Sammlung → 404 (`collection not found`); ohne Leserecht → 403 (`permission denied`).
### actions_list
Signatur: `namespace*`. Scope: Lese-Recht auf den Namensraum oder je Aktion. Methode: `GET`.
```json title="actions_list — tools/call"
{
"jsonrpc": "2.0",
"id": 24,
"method": "tools/call",
"params": {"name": "actions_list", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: ohne `namespace` → 400 (`namespace is required`); Aktionen ohne Recht fehlen still in der Liste.
### action_describe
Signatur: `namespace*`, `action*`. Scope: `namespace.<ns>._actions.<name>.read`. Methode: `GET`.
```json title="action_describe — tools/call"
{
"jsonrpc": "2.0",
"id": 25,
"method": "tools/call",
"params": {
"name": "action_describe",
"arguments": {"namespace": "<ns>", "action": "versand"}
}
}
```
Fehler: unbekannte Aktion → 404 (`action not found`); ohne Aktions-Scope → 403.
### jobs_list
Signatur: `namespace*`. Nur Configs, kein Trigger. Scope: `project.read`. Methode: `GET`.
```json title="jobs_list — tools/call"
{
"jsonrpc": "2.0",
"id": 26,
"method": "tools/call",
"params": {"name": "jobs_list", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: ohne `project.read`-Scope → 403; unbekanntes Projekt → 404 (`project not found`).
### openapi_get
Signatur: `namespace*`, optional `lang`. Laut Tool-Beschreibung öffentlich wie `GET /_openapi/:project`. Methode: `GET`.
```json title="openapi_get — tools/call"
{
"jsonrpc": "2.0",
"id": 27,
"method": "tools/call",
"params": {"name": "openapi_get", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: unbekanntes Projekt → 404 (`project not found`).
### system_meta_get
Signatur: optional `model` (`user`/`project`). Laut Tool-Beschreibung öffentlich wie `GET /_meta`. Methode: `GET`.
```json title="system_meta_get — tools/call"
{
"jsonrpc": "2.0",
"id": 28,
"method": "tools/call",
"params": {"name": "system_meta_get", "arguments": {"model": "user"}}
}
```
Fehler: ohne PAT → 403 (`mcp requires a personal access token`) — sonst antwortet das Werkzeug immer mit 200.
### agent_guide_get
Signatur: `namespace*`, optional `collection`, `action`, `lang`. Liest kuratiertes `meta.agent` (Projekt → Sammlung/Aktion) plus `yourPermissions`. Scope: je Ebene das passende Lese-Recht. Methode: `GET`.
```json title="agent_guide_get — tools/call"
{
"jsonrpc": "2.0",
"id": 29,
"method": "tools/call",
"params": {
"name": "agent_guide_get",
"arguments": {"namespace": "<ns>", "collection": "content"}
}
}
```
Fehler: ohne Sammlungs-Leserecht → 403 (`permission denied`); unbekanntes Projekt → 404 (`project not found`).
### assets_list
Signatur: `namespace*`. Scope: `project.read`. Methode: `GET`.
```json title="assets_list — tools/call"
{
"jsonrpc": "2.0",
"id": 30,
"method": "tools/call",
"params": {"name": "assets_list", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: unbekanntes Projekt → 404 (`project not found`).
### asset_read
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `namespace*`, `name*`, `path*`. Methode: `GET`.
```json title="asset_read — tools/call"
{
"jsonrpc": "2.0",
"id": 31,
"method": "tools/call",
"params": {
"name": "asset_read",
"arguments": {"namespace": "<ns>", "name": "bilder", "path": "logo.svg"}
}
}
```
Fehler: immer 400 (`use GET /_/:project/_/assets/:name/*path (public like web)`) — nutze stattdessen den Asset-Endpunkt.
### users_list
Signatur: optional `filter`, `limit`, `offset` (alle als String typisiert, `tools_system.go`). Scope: `user.read`. Methode: `GET`.
```json title="users_list — tools/call"
{
"jsonrpc": "2.0",
"id": 32,
"method": "tools/call",
"params": {"name": "users_list", "arguments": {"limit": "10"}}
}
```
Fehler: ohne `user.read`-Scope → 403; fremde Benutzer filtert die Eigentümer-Sicht heraus.
### user_get
Signatur: `id*`. Scope: `user.read`. Methode: `GET`.
```json title="user_get — tools/call"
{
"jsonrpc": "2.0",
"id": 33,
"method": "tools/call",
"params": {"name": "user_get", "arguments": {"id": "<id>"}}
}
```
Fehler: ohne `id` → 400 (`id is required`); unsichtbar → 403 über den Scope-Filter.
### user_create
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: optional `data`. Scope: `user.create`.
```json title="user_create — tools/call"
{
"jsonrpc": "2.0",
"id": 34,
"method": "tools/call",
"params": {"name": "user_create", "arguments": {"data": {"username": "neu"}}}
}
```
Fehler: immer 400 (`user write via MCP is REST-only in this phase, use REST /api/v1/user`) — nutze stattdessen `REST /api/v1/user`.
### user_update
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: optional `id`, `data`. Scope: `user.update`.
```json title="user_update — tools/call"
{
"jsonrpc": "2.0",
"id": 35,
"method": "tools/call",
"params": {
"name": "user_update",
"arguments": {"id": "<id>", "data": {"username": "neu"}}
}
}
```
Fehler: immer 400 (`user write via MCP is REST-only in this phase, use REST /api/v1/user`) — nutze stattdessen `REST /api/v1/user`.
### user_delete
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `id*`. Scope: `user.delete`.
```json title="user_delete — tools/call"
{
"jsonrpc": "2.0",
"id": 36,
"method": "tools/call",
"params": {"name": "user_delete", "arguments": {"id": "<id>"}}
}
```
Fehler: immer 400 (`user write via MCP is REST-only in this phase, use REST /api/v1/user`) — nutze stattdessen `REST /api/v1/user`.
### orgs_list
Signatur: optional `limit` (als String typisiert). Scope: `org.read`. Methode: `GET`.
```json title="orgs_list — tools/call"
{
"jsonrpc": "2.0",
"id": 37,
"method": "tools/call",
"params": {"name": "orgs_list", "arguments": {}}
}
```
Fehler: ohne `org.read`-Scope → 403; fremde Orgs filtert die Mitgliedschaft heraus.
### org_get
Signatur: `id*`. Scope: `org.read`. Methode: `GET`.
```json title="org_get — tools/call"
{
"jsonrpc": "2.0",
"id": 38,
"method": "tools/call",
"params": {"name": "org_get", "arguments": {"id": "<id>"}}
}
```
Fehler: ohne `id` → 400 (`id is required`); fremde Org → 403 über den Scope-Filter.
### teams_list
Signatur: optional `limit` (als String typisiert). Scope: `team.read`. Methode: `GET`.
```json title="teams_list — tools/call"
{
"jsonrpc": "2.0",
"id": 39,
"method": "tools/call",
"params": {"name": "teams_list", "arguments": {}}
}
```
Fehler: ohne `team.read`-Scope → 403.
### team_get
Signatur: `id*`. Scope: `team.read`. Methode: `GET`.
```json title="team_get — tools/call"
{
"jsonrpc": "2.0",
"id": 40,
"method": "tools/call",
"params": {"name": "team_get", "arguments": {"id": "<id>"}}
}
```
Fehler: ohne `id` → 400 (`id is required`).
### audit_query
Signatur: optional `filter`, `limit` (als String typisiert). Scope: `audit.read` plus Lese-Recht je Sammlung; System-Ereignisse bleiben für Nicht-Admins verborgen. Methode: `GET`.
```json title="audit_query — tools/call"
{
"jsonrpc": "2.0",
"id": 41,
"method": "tools/call",
"params": {"name": "audit_query", "arguments": {"limit": "20"}}
}
```
Fehler: ohne `audit.read`-Scope → 403.
### project_reload
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `namespace*`. Scope: `project.reload`.
```json title="project_reload — tools/call"
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {"name": "project_reload", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: bei `allowAdminActions: false` → 403 (`admin actions disabled`); sonst immer 400 (`admin tool "project_reload" is REST-only in this phase, use REST admin endpoints`).
### regenerate_search
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `namespace*`. Scope: `project.regenerate-search`.
```json title="regenerate_search — tools/call"
{
"jsonrpc": "2.0",
"id": 43,
"method": "tools/call",
"params": {"name": "regenerate_search", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: bei `allowAdminActions: false` → 403 (`admin actions disabled`); sonst immer 400 (`admin tool "regenerate_search" is REST-only in this phase, use REST admin endpoints`).
### backup_create
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `namespace*`. Scope: `project.backup`.
```json title="backup_create — tools/call"
{
"jsonrpc": "2.0",
"id": 44,
"method": "tools/call",
"params": {"name": "backup_create", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: bei `allowAdminActions: false` → 403 (`admin actions disabled`); sonst immer 400 (`admin tool "backup_create" is REST-only in this phase, use REST admin endpoints`).
### snapshots_list
<Badge text="REST-only" variant="caution" /> Führt per MCP nichts aus. Signatur: `namespace*`. Scope: `project.snapshot`. Methode: `GET`.
```json title="snapshots_list — tools/call"
{
"jsonrpc": "2.0",
"id": 45,
"method": "tools/call",
"params": {"name": "snapshots_list", "arguments": {"namespace": "<ns>"}}
}
```
Fehler: bei `allowAdminActions: false` → 403 (`admin actions disabled`); sonst immer 400 (`admin tool "snapshots_list" is REST-only in this phase, use REST admin endpoints`).
## REST-only-Ausnahmen
Neun Werkzeuge antworten nur mit einem Verweis auf REST — sie führen per MCP nichts aus:
| Werkzeug | Stattdessen |
|---|---|
| `user_create`, `user_update`, `user_delete` | `REST /api/v1/user` |
| `project_reload` | Admin-Reload-Endpunkt |
| `regenerate_search` | Admin-Suchindex-Endpunkt |
| `backup_create` | Admin-Backup-Endpunkt |
| `snapshots_list` | Admin-Snapshots-Endpunkt |
| `file_read` | `GET /_/:project/:collection/:id/:field/*path` |
| `asset_read` | `GET /_/:project/_/assets/:name/*path` |
Erkennbar an `"REST-only in this phase"` in der Beschreibung von `tools/list`.
`allowAdminActions` steuert nur, ob Admin-Werkzeuge überhaupt sichtbar sind — die Sperre bleibt.
## Eigene Agent-Anleitung pflegen (`meta.agent`)
`agent_guide_get` liest, was du in `api/config.yml` und pro Sammlung pflegst.
Pflege dort Zweck, Pflichtfelder, Lookups, Workflows und Fallstricke — der Agent folgt dem Wortlaut.
```yaml title="api/config.yml — meta.agent (Starter)"
meta:
agent:
description: "Tibi Svelte starter: Svelte 5 SPA with pagebuilder blocks, goja SSR, per-language navigation."
frontend:
lookup: "content: blocks.heroImage.image:medialib, blocks.image:medialib; navigation: elements.page:content"
designGuide: "Future free code/HTML blocks: add block type in content.blocks …"
workflows: ["Create page (…) -> fill blocks -> link navigation -> … -> verify /de plus anonymous SSR …"]
hints: ["Public reads need active:true plus publication window (filter_public) …"]
```
```yaml title="api/collections/content.yml — meta.agent pro Sammlung"
meta:
agent:
purpose: "CMS pages rendered by App.svelte + BlockRenderer (hero, features, richtext, accordion, contact-form)."
instructions: "Required: name, path, lang. Always set translationKey …"
fieldHints:
path: "lowercase, e.g. /ueber-uns; stored without language prefix"
workflows: ["filter before create (path+lang) to stay idempotent; 409 means exists", "…"]
hints: ["richtext image only renders when _lookup.image.file.src exists", "…"]
```
<Steps>
1. `purpose` + `instructions` schreiben: Was rendert die Sammlung, welche Felder sind Pflicht?
2. `lookup`-Zeile pflegen: Welche Fremdschlüssel muss der Agent immer mitladen?
3. `workflows` pflegen: Erstellen → verknüpfen → anonym verifizieren (PAT-Erfolg allein zählt nicht).
4. `hints` pflegen: `active:true`-Regel, Pfad-Muster, Eindeutigkeit (`path+lang`, `translationKey+lang`).
5. Per `agent_guide_get` gegenlesen.
</Steps>
Zusatz: Lese-Werkzeuge akzeptieren `extra`-Parameter für eigene Query-Schlüssel aus `meta.query` (Hooks lesen sie per `ctx.request().query(name)`).
REST ignoriert das Schema (offen per Design) und nutzt es als Doku; MCP validiert (`query.go`, Kern- und Auth-Schlüssel sind reserviert).
## Fehler
| Antwort | Ursache | Lösung |
|---|---|---|
| `404 "mcp disabled"` | `mcp.enabled: false` (Route bleibt, Handler antwortet 404) | In `config.yml` aktivieren |
| `403` + PAT-Hinweis | JWT, Admin-Token oder anonym statt PAT | `X-Auth-Token: tibi_pat_…` senden |
| `-32601 method not found` | Unbekannte Methode | `initialize`, `tools/list`, `tools/call` nutzen |
| `-32601 unknown tool` | Tippfehler im Werkzeugnamen | Namen aus `tools/list` kopieren |
| `-32003` | Scope oder Live-Recht fehlt | PAT-Scopes und [Rechte](/projekt/rechte-tokens/) prüfen |
| `-32602 invalid params` | Pflicht-Argument fehlt | `collection_describe`-Schema bzw. [Fehlerbilder](/reference/fehlercodes/) lesen |
| Leere Listen trotz Daten | `maxResultItems` (Standard 100) oder `filter_public`-Hook | Limit erhöhen bzw. `active:true` + Zeitraum prüfen |
Verifiziert am 68b42785 2026-10-16 (tibi-server: tools_crud, tools_discovery, tools_system, method, query, guards, registry, server, mcp_handler, mcp_tools_impl, setup), am e3f92eb 2026-10-14 (tibi-svelte-starter: config.yml und content.yml meta.agent).
+119
View File
@@ -0,0 +1,119 @@
---
title: 'Navigation und Sprachen'
description: 'Navigations-Collection, Sprachfelder, translationKey, Pfad-Regeln.'
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
Navigation und Sprachen hängen zusammen: Die `navigation`-Sammlung hält je Sprache einen Baum, `content` hält je Sprache einen Eintrag.
Suche-Begriffe: Navigation anlegen, Sprache umschalten, translationKey.
## Navigations-Dokumente
`api/collections/navigation.yml` kennt genau vier Singleton-Bäume: `type` (`header`, `footer`) mal `language` (`de`, `en`) — abgesichert per Unique-Index `type_language`.
```yaml title="navigation.yml — Felder (gekürzt)"
# navigation.yml — Felder (gekürzt)
fields:
- name: language # de | en, Pflicht
- name: type # header | footer, Pflicht
- name: elements # object[]: {name, page?, external?, externalUrl?, hash?, elements?}
```
| Element-Art | Felder | Beispiel |
|---|---|---|
| Intern | `name` + `page` (= `content`-ID) | `{name: "Start", page: "<content.id>"}` |
| Extern | `name` + `external: true` + `externalUrl` | `{name: "Blog", external: true, externalUrl: "https://…"}` |
| Anker | zusätzlich `hash` ohne `#`, Muster `^[a-zA-Z0-9_-]+$` | `{name: "Kontakt", page: "<id>", hash: "kontakt"}` |
| Unterpunkt | geschachtelt in `elements.elements` | Header max. 2 Ebenen, Footer max. 1 |
<Steps>
1. Lege je Sprache einen Header und einen Footer an, je mit `active:true` (Pflicht trotz fehlendem Schema-Feld — siehe Kasten unten).
2. Löse Seiten mit Lookup auf:
```text title="Seiten per Lookup auflösen"
GET …/navigation?lookup=elements.page:content
```
3. Baue den Link aus dem Lookup: `href` = lokalisierter `_lookup.page.path` plus `hash`. Den `/de`-Präfix nie in die DB schreiben.
</Steps>
<Aside type="caution" title="active:true auch hier">
`filter_public.js` filtert anonym `active:true` — `navigation.yml` deklariert kein solches Feld.
Ohne gespeichertes `active:true` liefert die öffentliche Abfrage `count 0` (Vorfall 2026-10-05), mit Token sieht alles gut aus.
Lege `active:true` auf jedem Navigations-Dokument ab.
</Aside>
## Sprachmodell: lang + translationKey
`content.yml` nutzt Entry-i18n: eine Sprache je Eintrag, verbunden über den Schlüssel.
```yaml title="content.yml — i18n-Block (gekürzt)"
# content.yml — i18n-Block (gekürzt)
meta:
i18n:
entry:
languageField: lang
groupField: translationKey
```
| Regel | Konkret |
|---|---|
| `lang` | Sprachkürzel, z. B. `de`, `en` |
| `translationKey` | **Pflicht**, z. B. `home`, `ueber-uns` — pro Sprache eindeutig (Unique-Index `translationKey` + `lang`) |
| Zweiter Datensatz ohne Schlüssel | Kollidiert auf `null` — kein „geht schon gut" |
| `path` | Ohne Sprachpräfix gespeichert (`/ueber-uns`), URL mit Präfix (`/de/ueber-uns`) |
| `alternativePaths[].path` | Alte Pfade, gleiches Muster wie `path` |
Die Feld-i18n (Tabs je Feld, Test-Plan `34-i18n-fields`) und Entry-i18n (eine Sprache je Eintrag, Test-Plan `35-entry-i18n`) pflegst du im tibi-admin; das Datenmodell aus Projekt-Sicht ist immer `lang` + `translationKey`.
## Pfad-Regeln
```text title="Pfad-Muster für path und alternativePaths"
^/[a-z0-9-/]*$ → gilt für path und alternativePaths[].path
```
- Nur Kleinbuchstaben, Ziffern, `-` und `/`; keine Großbuchstaben, keine Umlaute (`/ueber-uns` statt `/Über-uns`).
- Unique-Index `path` + `lang`: gleicher Pfad in `de` und `en` ist okay, zweimal in `de` nicht.
- SSR streift den Präfix vor der Suche ab und liefert die kanonische URL zurück (`hooks/config.js` → `ssrValidatePath`).
## active:true auch hier (Checkliste)
| Sammlung | Ohne `active:true` öffentlich … |
|---|---|
| `content` | … filtern `filter_public` + SSR-Prüfung den Eintrag weg |
| `medialib` | … bleibt `_lookup` leer, Bilder verschwinden (siehe [Dateien und Bilder](/projekt/dateien-bilder/)) |
| `navigation` | … ist Header/Footer leer |
## Prüfung: anonyme SSR-Nav-Zahl
<Tabs>
<TabItem label="Deutsch + Englisch testen">
```text title="Navigation je Sprache prüfen (anonym)"
GET …/navigation?filter={"language":"de"}
GET …/navigation?filter={"language":"en"}
# anonym (ohne Token) müssen beide je einen Header und einen Footer liefern
```
</TabItem>
<TabItem label="SSR-Cache prüfen">
```text title="SSR-Cache prüfen"
GET …/ssr?url=/de/
# im HTML: window.__SSR_CACHE__ enthält das Ergebnis je Request (cacheKey mit data + count) — Navigation darin je Sprache mit count > 0 prüfen; die Abhängigkeiten selbst stehen separat im ssr-Dokument (Feld dependencies)
```
</TabItem>
</Tabs>
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Öffentliche Nav leer, mit Token voll | `active:true` fehlt | Auf allen vier Bäumen setzen |
| `409` beim zweiten Spracheintrag | `translationKey` fehlt oder doppelt | Eindeutigen Schlüssel je Sprache vergeben |
| `400` beim Speichern | Pfad mit Großbuchstaben/Umlaut | Kleinschreiben, Muster einhalten |
| Falsche Sprache im SSR | `lang`-Feld passt nicht zum Präfix | `lang` und URL-Präfix angleichen |
Weiter: [Lesen und suchen](/projekt/abfragen-lesen/), [Dateien und Bilder](/projekt/dateien-bilder/), [Rechte und Tokens](/projekt/rechte-tokens/).
Verifiziert am e3f92ebf 2026-10-14 (tibi-svelte-starter), am 2b091d89 2026-10-13 (tibi-admin-nova, Test-Pläne 34-i18n-fields, 35-entry-i18n)
+248
View File
@@ -0,0 +1,248 @@
---
title: 'Pagebuilder-Anbindung (Dev)'
description: 'Block-Registry, CSS-Umschreibung, Admin-Vorschau und Screenshot-Pipeline aus Entwickler-Sicht.'
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
Die Vorschau im **tibi-admin** ist **nicht** deine Website.
Sie rendert vereinfachte Vorschau-Blöcke in einem Shadow-DOM.
Die echte Website baut der `BlockRenderer` im Starter per SSR (siehe [Website mit SSR](/projekt/ssr-website/)).
```text title="Vorschau vs. Website: zwei Wege aus blocks[]"
Eintrag (blocks[]) → Registry (Vorschau) → Shadow-DOM im Editor
Eintrag (blocks[]) → BlockRenderer.svelte → echtes HTML im Projekt
```
## Registry-Vertrag
Die Registry ist eine JS-Datei mit einem Block-Namen pro Schlüssel.
Jeder Block liefert `render`, dazu `update` und `destroy` im Handle sowie ein `css`-Array.
Pfad steht in `meta.pagebuilder.blockRegistry.file` (Feld oder Collection).
```js title="Referenz-Registry (corporate-blocks/registry.js)"
function defineBlock(renderContent) {
return {
render(container, row, context) {
const el = document.createElement("div")
renderContent(el, row, context)
container.appendChild(el)
return {
update(nextRow, nextContext) {
renderContent(el, nextRow, nextContext)
},
destroy() {
el.remove()
},
}
},
css: ["/assets/mock/corporate-blocks/blocks.css"],
}
}
export default registry
export { registry as blockRegistry }
```
| Teil | Vertrag (`BlockMount.ts`, `load-registry.ts`) |
|---|---|
| Export | `default` oder `blockRegistry` als `BlockRegistry`-Map |
| `render(container, row, context)` | Baut das DOM, gibt `{ update, destroy }` zurück |
| `update(row, context)` | Neue Daten bei gleichem Typ — kein DOM-Neuaufbau |
| `destroy()` | Räumt auf (Listener, Element entfernen) |
| `css` | Array mit Stylesheet-URLs, wird vor dem Mount vorgeladen |
| Fehlt der Typ | `render()` gibt `false` zurück → Blueprint-Karte statt Vorschau |
Laden ist lazy und gecacht (`loadBlockRegistry`).
Schlägt der Import fehl, zeigt der Editor den Blueprint-Modus — kein Fehler, nur keine Vorschau.
## CSS-Umschreibung
Dein Block-CSS läuft durch `transformPagebuilderCssLenientAsync` (`pagebuilder-css-transform.ts`).
Grund: Das CSS landet isoliert im Shadow-DOM, nicht im Seiten-`<head>`.
| Umschreibung | Warum |
|---|---|
| `@media` mit Breiten-Bedingung → `@container` | Breakpoints reagieren auf die **Vorschau-Breite**, nicht aufs Editor-Fenster |
| `html` / `body` / `:root` → `:host` | Diese Selektoren treffen im Shadow Root nichts |
| Relative `url()` → absolut (`baseUrl`) | Inline-Styles lösen relativ sonst gegen das Admin-Dokument auf |
| `@import` wird eingebettet (`fetchCss`) | Konstruierte Sheets werten `@import` im Browser nicht aus |
Regeln dazu: Nur komplette Queries werden konvertiert (Teil-Umschreibung verboten).
`device-width` und `not` bleiben `@media`.
Unparsebares CSS lädt **unverändert** plus `console.warn` mit Zeile und Spalte.
Abschalten je Block, Feld oder Collection:
```yaml title="content.yml — Opt-out (load-registry.ts: Block → Feld → Collection → true)"
meta:
pagebuilder:
cssTransform: false
```
<Aside type="caution">
Bei `cssTransform: false` bleibt die Container-Basis (`container-type: inline-size`) aktiv.
Sonst greift kein `@container` im aufbereiteten CSS.
</Aside>
## Lookup in der Vorschau
Die Vorschau hydriert Fremdschlüssel selbst (`hydratePagebuilderRow` in `lookup-hydration.ts`).
Das Muster kennst du aus [Lesen und suchen](/projekt/abfragen-lesen/): IDs plus `_lookup`-Objekt.
<Tabs>
<TabItem label="Prinzip">
<Steps>
1. IDs aus `foreignKey`/`foreignMedia`-Feldern sammeln (z. B. `blocks.heroImage.image:medialib`).
2. Einträge per `getEntriesByIds` laden (gecacht pro `namespace:collection:Feld:id`).
3. Ergebnis unter `row._lookup.<feld>` einbetten — der Registry-Block liest daraus.
4. Datei-URLs per `normalizeLookupUrl` auflösen (relativ → Projekt-Basis).
</Steps>
</TabItem>
<TabItem label="Lesen im Block">
```js title="registry.js — Lookup lesen (getLookup / getMediaSource)"
function getLookup(row, key) {
return row?._lookup?.[key] || null
}
function getMediaSource(row, key = "mediaRef") {
const lookup = getLookup(row, key)
return lookup?.file?.src || lookup?.url || null
}
```
Ohne Lookup steht im Feld nur die ID.
Ohne Leserecht oder ohne `active:true` am Ziel bleibt `_lookup` leer — gleiche Regel wie in der REST-Abfrage.
</TabItem>
</Tabs>
Bildgrenze: `MedialibImage` ist die einzige Bild-Grenze im Starter (`file.src` oder Data-URI).
Details: [Dateien und Bilder](/projekt/dateien-bilder/), [Medien](/admin/medien/).
## Screenshot- und Thumbnail-Pipeline
Der Listen-`image`-Eintrag (`meta.preview.image: _pagebuilderThumbnail`) kommt aus gerenderten Screenshots.
Kette:
<Steps>
1. Regel lesen: `meta.pagebuilder.screenshot` in `content.yml` (`field: blocks`, `fileField: _pagebuilderThumbnail`).
2. Registry laden und Block im `PagebuilderScreenshotHost` rendern (versteckt, pro Regel).
3. Schriften und Bilder abwarten (Timeout je 4 s, Render-Timeout 12 s).
4. Per `modern-screenshot` (`domToCanvas`) aufnehmen.
5. Pfad bauen: `buildVersionedPagebuilderScreenshotPath` → `<feld>.v-<hash>.<ext>` (`webp`/`jpg`).
6. Ins `file`-Feld schreiben (`applyFileField`).
</Steps>
```yaml title="content.yml — Screenshot-Regel (Starter)"
meta:
pagebuilder:
screenshot:
- field: blocks
fileField: _pagebuilderThumbnail
```
Seed für Mock-Daten: `scripts/seed-mock-pagebuilder-screenshots.js` (Playwright, Thumbnails für die Mock-Registry).
## Eigener Block
<Steps>
1. Typ in `api/collections/content.yml` anlegen: `choices`-Eintrag unter `blocks.type` plus Felder mit `dependsOn`-Eval auf `$parent.type`.
2. Svelte-Komponente in `frontend/src/blocks/` anlegen (z. B. `QuoteBlock.svelte` mit `$props()`, SSR-sicher mit Window-Guards).
3. `case` in `BlockRenderer.svelte` ergänzen (`block.type === "quote"` → `<QuoteBlock {block} />`).
4. Vorschau-Block in der Registry-Datei ergänzen (gleicher Schlüssel, `defineBlock`-Vertrag, eigenes CSS).
5. Seite speichern — Vorschau und Screenshot laufen automatisch über die neue Registry.
</Steps>
```yaml title="content.yml — Feld nur für den eigenen Typ zeigen"
- name: quote
type: string
meta:
dependsOn:
eval: $parent.type == 'quote'
```
```svelte title="BlockRenderer.svelte — Case ergänzen"
{:else if block.type === "quote"}
<QuoteBlock {block} />
```
Design-Regeln aus dem Starter (`api/config.yml`, `meta.agent.designGuide`): Svelte-5-Runes (`$props`, nie `block` destrukturieren), Tailwind v4 kanonisch, i18n per `$_`, Bilder nur über `MedialibImage`.
## Fehler
### Editor-Overlay (`PagebuilderOverlay.svelte`)
Der Klick auf einen Block öffnet einen Fullscreen-Split-Editor auf dunklem Hintergrund (`backdrop-blur-md`, eigenes Layer per `pushLayer`, `focusTrap`, `role="dialog"` mit `aria-label` aus `admin.pagebuilder.editorAria`).
| Bereich | Inhalt |
|---|---|
| Links | Formular-Karte (`FieldRenderer` mit `subFields` des Blocktyps) |
| Rechts | Live-Vorschau (`RowPreview`) im skalierten Rahmen, passt sich automatisch an Breite und Höhe an |
| Toolbar links | Block-Label mit Farbpunkt plus Blocknummer (`#1`, `#2`, …) |
| Toolbar Mitte | Modus-Umschalter: **Nur Formular** (`viewForm`), **Formular und Vorschau** (`viewBoth`), **Nur Vorschau** (`viewPreview`) |
| Toolbar rechts | **OK** mit `ESC`-Badge, Titel **Schließen (ESC)** (`closeTitle`) |
Verhalten:
- **Speichern** gibt es im Overlay nicht — jede Eingabe schreibt per `onchange` sofort in die Zeile. **OK** und `ESC` schließen nur. `ESC` löst vorher `blur` aus; die eigentliche Prüfung läuft beim Verlassen der Drilldown-Ebene und vor dem Speichern (`entry:before-save`) über `validateAllFields`.
- **Validierung:** Beim Verlassen einer Drilldown-Ebene und vor dem Speichern (`entry:before-save`) prüft `validateAllFields` den aktuellen Pfad-Scope.
- **Breadcrumbs:** `DrawerBreadcrumb` (Layer-Stapel) oben, `DrillDownBreadcrumb` im Formular bei verschachtelten Objekten. `ESC` geht zuerst eine Drilldown-Ebene zurück und schließt erst am Root.
- **Schmal (< 768 px):** `both` entfällt, der Editor fällt auf **Nur Formular** zurück.
### Viewport-Steuerung (`PagebuilderViewportControl.svelte`)
Sit-observer unter der Vorschau (Editor) bzw. in der Übersichts-Toolbar. Sie simuliert die Seitenbreite, damit `@container`-Queries wie auf der echten Seite greifen.
| Preset | Breite |
|---|---|
| **Desktop** (`viewportDesktop`) | 1280 px |
| **Tablet** (`viewportTablet`) | 768 px |
| **Mobil** (`viewportPhone`) | 375 px |
| Freier Slider | 320–1920 px, Anzeige in `px`, Skalierung in `%` bei Verkleinerung |
Startwert ist `defaultViewport` (Feld-`meta.pagebuilder.defaultViewport` → Collection → `1280`); Preset-Namen oder Zahlen sind erlaubt.
### Übersichts-Overlay und `drillDown` (`PagebuilderOverviewOverlay.svelte`, `PagebuilderWidget.svelte`)
| Modus | Wann | Darstellung |
|---|---|---|
| `overlay` (Standard) | `meta.drillDown` gesetzt oder fehlt | Kompakt-Thumbnail (`PagebuilderThumbnail`) im Formular; Klick öffnet das Fullscreen-Übersichts-Overlay mit Blockliste, **Block hinzufügen**-Knopf und Viewport-Steuerung |
| `inline` | `meta.drillDown: false` am Feld | Blockliste rendert direkt im Formular, kein Thumbnail, kein Overlay |
Der Block-Editor (`PagebuilderOverlay`) öffnet sich in beiden Modi identisch obendrauf.
Block-Management in der Übersicht:
| Aktion | Wie |
|---|---|
| Hinzufügen | **Block hinzufügen** → Typ im Picker wählen — der neue Block öffnet sofort im Editor |
| Öffnen | Klick auf die Block-Vorschau |
| Sortieren | Drag-Handle (**Ziehen zum Sortieren**) oder Menü (**Aktionen**): **Nach oben**, **Nach unten** |
| Duplizieren / Löschen | Gleiches Menü: **Duplizieren**, **Löschen** |
| Schließen | **OK** oder `ESC` (oberstes Layer schließt zuerst) |
Leere Liste zeigt **Keine Inhaltsblöcke vorhanden**; die Toolbar zählt **Keine Blöcke** bzw. **N Blöcke** (`noBlocks`, `blocks`).
### BlueprintCard-Fallback (`BlueprintCard.svelte`)
Liefert `render()` `false` (Typ fehlt in der Registry) oder schlägt der Registry-Import fehl, zeigt `RowPreview` statt der Vorschau eine Schema-Karte: Block-Label als Badge, bis zu vier befüllte Felder als kompakte Werte (Text gekürzt, Richtext ohne HTML, Datei als Pfad, Farbe als Swatch), Rest als **+N weitere** (`blueprintMoreFields`), Kinder-Zähler (`blueprintChildren`), ohne Werte **Keine Daten** (`blueprintNoData`). Kein Fehlerzustand — nur keine Grafik.
| Symptom | Ursache | Lösung |
|---|---|---|
| Blueprint-Karte statt Vorschau | Registry-Pfad falsch oder Typ fehlt in der Map | `blockRegistry.file` prüfen, Schlüssel = `type`-Wert |
| CSS „blutet" in den Editor | `cssTransform: false` oder Parse-Fehler (Warnung in der Konsole) | Warnung mit Zeile/Spalte lesen, CSS fixen |
| Bild leer in der Vorschau | Lookup-ID ohne Ziel oder Ziel ohne `active:true` | Medialib-Eintrag prüfen, anonyme Abfrage gegenlesen |
| Altes Thumbnail | Screenshot-Regel fehlt oder `fileField` falsch | `meta.pagebuilder.screenshot` in `content.yml` prüfen |
| Felder erscheinen nicht | `dependsOn`-Eval auf `$parent.type` fehlt | Eval wie oben setzen, Typ-Schreibweise exakt |
Verifiziert am 2b091d89 2026-10-16 (tibi-admin-nova: PagebuilderOverlay, PagebuilderViewportControl, PagebuilderOverviewOverlay, BlueprintCard, PagebuilderWidget, de.json admin.pagebuilder.*), am e3f92eb 2026-10-14 (tibi-svelte-starter: content.yml, BlockRenderer, config.yml designGuide).
+154
View File
@@ -0,0 +1,154 @@
---
title: 'Rechte und Tokens'
description: 'Permissions, Token-Köpfe, active:true-Falle, Orgs und Teams im Überblick.'
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Wer lesen und schreiben darf, steht in jeder Sammlung unter `permissions`.
Welchen Kopf du mitschickst, entscheidet, welche Rolle du bist.
Beispiele stammen aus `content.yml`, `comment.yml`, `api/hooks/config.js` und `filter_public.js`.
## Permission-Blöcke
```yaml title="content.yml — öffentlich lesen, Rest für Nutzer und Token"
# aus content.yml — öffentlich lesen, Rest für angemeldete Nutzer und Token
permissions:
public:
methods:
get: true
user:
methods:
get: true
post: true
put: true
delete: true
"token:${ADMIN_TOKEN}":
methods:
get: true
post: true
put: true
delete: true
```
| Block | Wer ist das | Beispiel |
|---|---|---|
| `public` | Anonyme Anfragen ohne Kopf | `content`, `navigation`: `get: true` |
| `user` | Angemeldet per JWT (`X-Auth-Token`) | `content`, `comment`: volles CRUD |
| `"token:${ADMIN_TOKEN}"` | Skript mit statischem Token (`Token`-Kopf) | alle Starter-Sammlungen: volles CRUD |
- `methods` kennt `get`, `post`, `put`, `delete`.
- Fehlt ein Block, darf die Rolle nichts.
- `tag.yml` hat zusätzlich `post`/`put`/`delete`-Hooks (`clear_cache.js`), aber keinen Lese-Hook.
## Token-Köpfe
Vier Köpfe, vier Zwecke. Verwechsle sie nicht.
| Kopf | Wert | Wofür |
|---|---|---|
| `Token` | `ADMIN_TOKEN` aus `api/config.yml.env` | Sammlungs-CRUD, wenn der Permission-Schlüssel `token:${ADMIN_TOKEN}` heißt |
| `X-Auth-Token` | JWT nach Login | Nutzer-Aktionen (`user:`-Rechte) |
| `X-Admin-Token` | Server-Admin-Token | System-Ebene: Projekte, Reload, Server-Ops |
| `Authorization: Bearer …` | `ADMIN_TOKEN` | Reverse-Proxy-Reload auf `LIVE_URL`/`STAGING_URL` (projektlokal, kein Server-API-Kopf) |
<Tabs>
<TabItem label="Lesen (öffentlich)">
```text title="Lesen ohne Kopf (public.get)"
GET /api/v1/_/<namespace>/content?filter={"lang":"de"}
# kein Kopf nötig — public.get ist true
```
</TabItem>
<TabItem label="Schreiben (Token)">
```text title="Schreiben mit Token (comment)"
POST /api/v1/_/<namespace>/comment
Token: <ADMIN_TOKEN>
{"contentId": "<id>", "author": "Demo", "message": "Hallo", "active": true}
```
</TabItem>
<TabItem label="Schreiben (Nutzer)">
```text title="Schreiben als Nutzer (JWT)"
PUT /api/v1/_/<namespace>/content/<id>
X-Auth-Token: <jwt>
{"name": "Startseite"}
```
</TabItem>
</Tabs>
<Aside type="caution">
`ADMIN_TOKEN` kann **keine** Projekte anlegen.
Er gilt nur für Sammlungen mit passendem `token:`-Eintrag.
Projekte und Reload brauchen `X-Admin-Token` auf System-Ebene.
</Aside>
## active:true-Falle
Anonyme Leser bekommen automatisch einen Zusatzfilter.
`filter_public.js` hängt ihn an jede Abfrage ohne Login:
```js title="api/hooks/filter_public.js — nur ohne Login aktiv"
// api/hooks/filter_public.js — nur ohne Login aktiv
hookResponse.filter = publishedFilter; // bzw. $and mit deinem filter
```
```js title="api/hooks/config.js — der Filter im Kern"
// api/hooks/config.js — der Filter im Kern
const publishedFilter = {
active: true,
$or: [
{ publication: { $exists: false } },
{ publication: null },
// … plus Fenster publication.from <= jetzt <= publication.to
],
}
```
Das heißt: **öffentlich sichtbar ist nur `active: true` innerhalb des Veröffentlichungsfensters.**
| Sammlung | Falle | Folge ohne `active:true` |
|---|---|---|
| `content` | Feld `active` existiert, Hook filtert | Seite öffentlich unsichtbar |
| `navigation` | Feld `active` existiert **nicht** im Schema, Hook filtert trotzdem | Header/Footer öffentlich leer (`count 0`), SSR-Cache ohne Navigation |
| `medialib` | Feld `active` existiert **nicht** im Schema, Hook filtert trotzdem | Lookups öffentlich unaufgelöst (nur IDs, kein `_lookup`), Hero zeigt Verlauf, Richtext-Bilder fehlen |
| `tag` | Kein Lese-Hook | Keine Falle — `active` unnötig |
| `comment` | Feld `active` existiert, Hook filtert | Nur freigeschaltete Kommentare sind öffentlich |
Regel: Lege auf **jedem** `content`-, `navigation`-, `medialib`- und `comment`-Dokument `active: true` ab.
Mit Token oder JWT siehst du alles — prüfe Anonymes daher immer ohne Kopf (z. B. Inkognito-Fenster).
## Orgs und Teams im Überblick
Größere Installationen gruppieren Nutzer und Projekte:
- **Org**: Mandant oben (`matrixorga`, `matrixorgb` im Test).
- **Team**: Gruppe in einer Org, mit oder ohne Rechte (z. B. `teama1` mit `editor`).
- **Projekt**: hängt an einer Org (`orgId`) und kennt seine Teams.
- **Org-Admin**: Rolle `admin` in einer Org — darf org-eigene Projekte, aber nichts in fremden Orgs.
```text title="Org-, Team- und Projekt-Struktur (Beispiel)"
Org A ─┬─ Team A1 (editor) ─┬─ Projekt A
└─ Team A2 (ohne) ─┘
Org B ─── Team B1 (editor) ─── Projekt B
```
Mitglieder von Team A1 kommen in Projekt A, aber nicht in Projekt B.
Ohne Org-Zugehörigkeit geht gar nichts.
Details regelt der Server-Code (`access_control_matrix_test.go`) — für normale Projekte reicht: ein Projekt, ein Team, `ADMIN_TOKEN` für Skripte.
## Fehlerbilder
| Antwort | Ursache | Lösung |
|---|---|---|
| `permission denied` | Rolle darf die Methode nicht oder Projektion ist fremd | `permissions`-Block und gesendeten Kopf prüfen |
| `project not found` | Namespace falsch oder Projekt nicht registriert | [Neues Projekt](/einstieg/neues-projekt/) Schritt 5 |
| `lookup permission denied` | Leserecht auf der Ziel-Sammlung fehlt | Ziel-Sammlung braucht `public.get` oder gleichen Kopf |
| Öffentlich leer, mit Token voll | `active:true` fehlt oder `publication`-Fenster liegt in der Zukunft/Vergangenheit | `active:true` setzen, `publication.from`/`publication.to` prüfen |
| Falscher Kopf, richtige Rolle | z. B. `Token:` statt `X-Auth-Token:` bei Nutzer-Rechten | Tabelle oben: Kopf an Rolle anpassen |
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter, Hooks und Permissions), am 68b42785 2026-10-15 (tibi-server, access_control_matrix_test.go, Lookup-/Projektions-Prüfung).
@@ -0,0 +1,166 @@
---
title: 'Sammlungen anlegen'
description: 'Eigene Collection in api/collections definieren: Schema, Meta, Indizes, Suche.'
---
import { Aside, Steps, FileTree } from '@astrojs/starlight/components';
Eine **Sammlung** (Collection) ist eine Tabelle deines Projekts.
Du legst sie als YAML-Datei in `api/collections/` an.
Der Dateiname ist frei, entscheidend ist das Feld `name`.
Beispiel-Sammlungen zum Abschauen: `content`, `navigation`, `medialib`, `tag`, `comment`.
<FileTree>
- api/
- collections/
- content.yml
- navigation.yml
- medialib.yml
- tag.yml
- comment.yml
</FileTree>
## Minimal-Beispiel
Die kleinste Sammlung braucht `name` **und `permissions`** (beides Pflicht laut Schema) — `fields` ist dagegen optional.
Abgeschaut von `tag.yml` (vereinfacht):
```yaml title="api/collections/notiz.yml — Minimal-Beispiel (vereinfacht aus tag.yml)"
# api/collections/notiz.yml
# yaml-language-server: $schema=../../tibi-types/schemas/config/collection.schema.json
name: notiz
uploadPath: ../media/notiz
audit:
enabled: true
permissions:
public:
methods:
get: true
user:
methods:
get: true
post: true
put: true
delete: true
indexes:
- name: name_unique
key:
- name
unique: true
background: true
search:
- name: default
mode: regex
fields: [name, text]
fields:
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
- name: text
type: string
meta:
label: { de: "Text", en: "Text" }
```
<Steps>
1. Datei `api/collections/<name>.yml` anlegen, Schema-Zeile aus dem Beispiel kopieren.
2. `name` kleinschreiben, keine Umlaute und keine Leerzeichen (`content`, `tag`, `comment` als Vorbild).
3. `permissions` setzen — sonst liest niemand die Daten (Details: [Rechte und Tokens](/projekt/rechte-tokens/)).
4. `uploadPath` setzen, auch wenn die Sammlung keine Dateien enthält (`../media/<name>` wie in allen Starter-Sammlungen).
5. Projekt neu laden, damit der tibi-server die Datei einliest.
6. Testen: `POST` anlegen, doppelten Wert schicken → `409` bei Unique-Index.
</Steps>
## Meta: Label, Gruppe, Icon, Vorschau
`meta` steuert, wie tibi-admin die Sammlung anzeigt.
Pflicht ist nichts davon — aber ohne `label` rätst du im Admin.
| Schlüssel | Wirkung | Beispiel aus dem Starter |
|---|---|---|
| `label` | Anzeigename (`de`/`en`) | `tag.yml`: `{ de: "Schlagwörter", en: "Tags" }` |
| `group` | Gruppierung in der Navigation | `content`, `content`, `community` (`comment.yml`) |
| `muiIcon` | Icon im Admin | `label`, `menu`, `chat`, `folder-multiple-image` |
| `imageUrl` | Vorschaubild für Karten | Unsplash-URL in `content.yml` |
| `preview` | Listenvorschau: `label`, `secondary`, `tertiary`, `image` | `content.yml`: `label: name`, `secondary: path`, `tertiary: lang` |
| `viewHint` | Darstellungsart im Admin | siehe unten |
| `sidebar` | Eigene Seitenleiste mit Gruppen | `content.yml`: `publishing`, `seo` |
| `i18n.entry` | Übersetzungs-Kopplung | `content.yml`: `languageField: lang`, `groupField: translationKey` |
Felder positionierst du mit `meta.position`: `main`, `sidebar:<gruppe>` oder `sidebar:settings`.
Beispiel: `lang` liegt in `content.yml` auf `sidebar:settings`, `active` auf `sidebar:publishing`.
`viewHint` aus dem Starter (geprüft, alle drei Varianten):
- `viewHint: cards` — Kartenansicht (`content.yml`).
- `viewHint.media` — Medienansicht mit Upload und KI-Alt-Text (`medialib.yml`).
- `viewHint.navigation` — Baumansicht mit `nodesField` und `declaredTrees` (`navigation.yml`, Bäume: Header/Footer je Sprache).
## Indizes und Suche
### Indizes
Jeder Eintrag in `indexes` braucht nur `key` (`name` ist freiwillig).
`key` ist eine Liste von Feldnamen, Text-Index mit `"$text:$**"`.
```yaml title="content.yml — Text-Index plus zwei Unique-Indizes"
# aus content.yml — Text-Index plus zwei Unique-Indizes
indexes:
- name: content_text
key:
- "$text:$**"
defaultLanguage: none
background: true
- name: path_lang
key:
- path
- lang
unique: true
background: true
- name: translation
key:
- translationKey
- lang
unique: true
background: true
```
`unique: true` schützt vor Doppelten: der zweite `POST` mit gleichem `path` + `lang` scheitert — modern mit `409`, im Legacy-Default (Standard) mit `500` (siehe [Fehlerbilder](/reference/fehlercodes/)).
So erzwingt der Starter auch einzelne Navigationsbäume (`type_language` in `navigation.yml`) und einzelne Tag-Namen (`name_unique` in `tag.yml`).
### Suche
Jeder Eintrag in `search` braucht nur `name` und `mode` (`fields` ist freiwillig).
Vier Modi kommen im Starter vor:
| Modus | Wofür | Beispiel |
|---|---|---|
| `regex` | Einfache Teilstringsuche | `tag.yml`, `navigation.yml`, `comment.yml`: `default` |
| `ngram` | Fehlertolerante Fuzzy-Suche | `content.yml`: `fuzzy` mit `autoRegenerate: true` |
| `vector` | Semantische Vektorsuche | `content.yml`: `semantic` mit `vector: { provider: bge_m3 }` |
| `combined` | Hybrid aus semantisch + fuzzy (RRF) | `content.yml`: `combined` mit `rrf: { k: 60, topK: 100 }` |
Abfrage dazu: `GET …/content?q=…&qName=combined`.
Details stehen unter [Lesen und suchen](/projekt/abfragen-lesen/).
## Häufige Fehler
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| `409` beim Anlegen | `unique`-Index verletzt (`path`+`lang`, `translationKey`+`lang`, Tag-`name`) | Vor `POST` per `filter` prüfen, ob der Datensatz schon existiert |
| `project not found` | Projekt nicht registriert oder Namespace falsch | Siehe [Neues Projekt](/einstieg/neues-projekt/) Schritt 5 |
| Validierung schlägt fehl | Pflichtfeld fehlt oder `pattern` verletzt (z. B. `path` ohne führenden `/`) | Feldregeln stehen unter [Felder und Widgets](/projekt/felder-widgets/) |
| Öffentlich leere Liste, mit Token voll | `active:true` fehlt — der `filter_public`-Hook filtert alles weg | Siehe [Rechte und Tokens](/projekt/rechte-tokens/) |
| Datei-Upload landet falsch | `uploadPath` fehlt oder zeigt ins falsche Verzeichnis | `../media/<name>` wie in allen Starter-Sammlungen setzen |
<Aside type="tip">
Lege neue Felder zuerst in einer Test-Sammlung an, nicht in `content`.
`content` hat Pagebuilder-Blöcke und drei Such-Indizes — Fehler dort tun mehr weh.
</Aside>
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter), am 4c55f82 2026-10-12 (tibi-types).
+178
View File
@@ -0,0 +1,178 @@
---
title: 'Website mit SSR'
description: 'Routen, Daten-Lookups, Blöcke, SSR-Bundle, Cache und Invalidierung im Starter.'
---
import { Aside, Tabs, TabItem, Steps, FileTree } from '@astrojs/starlight/components';
Deine Website ist eine Svelte-5-App mit Server-Side-Rendering.
Der **tibi-server** rendert sie pro URL vor und legt das HTML in der Sammlung `ssr` ab.
Container: Seiten-Routen, Daten-Lookups, Blöcke, SSR-Bundle, Cache.
<FileTree>
- frontend/src/
- App.svelte
- ssr.ts
- blocks/
- api/hooks/
- lib/app.server.js
</FileTree>
## Routen
| URL | Was passiert |
|---|---|
| `/` | Leitet auf die Browser-Sprache um (`/de/` oder `/en/`) |
| `/:lang/*` | z. B. `/de/`, `/de/ueber-uns` — Sprache plus Seitenpfad |
| Unbekannter Pfad | `NotFound.svelte` (404) |
- `content.path` steht **ohne** Sprachpräfix in der Datenbank (`/`, nicht `/de/`).
- Pfad-Regel: `^/[a-z0-9-/]*$` — klein, keine Umlaute.
- `translationKey` + `lang` und `path` + `lang` sind je eindeutig. Setze immer `translationKey`, sonst kollidiert die zweite Seite auf `null/de`.
- Sprache steckt im Pfad (`extractLanguageFromPath`), Titel und Meta kommen aus `contentEntry.meta`.
## Daten laden und Lookups
`App.svelte` lädt pro Seite drei Dinge über `getCachedEntries`:
```ts title="frontend/src/App.svelte — die drei Abfragen"
const CONTENT_MEDIA_LOOKUP = ["blocks.heroImage.image:medialib", "blocks.image:medialib"].join(",")
const NAVIGATION_CONTENT_LOOKUP = "elements.page:content"
// Inhalt: genau ein Treffer (limit 1), nur aktiv, mit Medien-Lookup und Kommentar-Zähler
getCachedEntries("content", {
filter: { lang, path: routePath, active: true },
limit: 1, sort: "sort",
aggregate: { collection: "comment", foreignField: "contentId", op: "count",
filter: { active: true }, as: "commentCount" },
lookup: CONTENT_MEDIA_LOOKUP,
})
// Navigation: je ein Eintrag header/footer mit aufgelösten Seitenpfaden
getCachedEntries("navigation", {
filter: { type: "header", language: lang }, lookup: NAVIGATION_CONTENT_LOOKUP, ...
})
```
| Lookup | Wirkung |
|---|---|
| `blocks.heroImage.image:medialib`, `blocks.image:medialib` | Bild-IDs werden zu `_lookup`-Objekten mit `file.src` |
| `elements.page:content` | Navigations-Elemente bekommen den Zielpfad (`_lookup.page.path`) |
| Kommentar-Aggregat | `commentCount` ohne zweite Abfrage |
<Aside type="caution">
Öffentlich sichtbar ist nur `active: true` — das gilt auch für `navigation` und `medialib`, obwohl deren Schema gar kein `active`-Feld kennt.
Ohne das Flag: Header/Footer leer (`count 0`), Bild-Lookups unaufgelöst (nur IDs).
Details unter [Rechte und Tokens](/projekt/rechte-tokens/).
</Aside>
## Blöcke
`BlockRenderer.svelte` mappt `content.blocks` auf Komponenten:
| `type` | Komponente | Hinweis |
|---|---|---|
| `hero` | `HeroBlock.svelte` | Ohne Bild: Verlauf (Gradient-Fallback) |
| `features` | `FeaturesBlock.svelte` | Icons über `FeatureIcon.svelte` |
| `richtext` | `RichtextBlock.svelte` | Bilder nur bei aufgelöstem `_lookup.image.file.src` |
| `accordion` | `AccordionBlock.svelte` | Aufklapp-Sektionen |
| `contact-form` | `ContactFormBlock.svelte` | Formular aus `widgets/` (`Form`, `Input`, `Select`, `Button`) |
- `hide: true` blendet einen Block aus.
- Unbekannte Typen werden still übersprungen — nur auf `localhost` erscheint ein gelber Hinweis.
- Bilder laufen ausschließlich über `MedialibImage.svelte` (`file.src` oder Data-URI, kein `?filter` auf Data-URIs).
## Eigener Block
<Steps>
1. Feld in `api/collections/content.yml` unter `blocks` ergänzen: `type` plus Felder mit `dependsOn: $parent.type` — so zeigt der Editor nur passende Felder.
2. Komponente in `frontend/src/blocks/` anlegen, z. B. `QuoteBlock.svelte`:
- Props per Runen: `let { block } = $props()` — `block` nie destrukturieren.
- Abgeleitete Werte mit `$derived`, Zustand mit `$state`.
- SSR-sicher schreiben: jeder `window`-Zugriff braucht einen Guard (`typeof window !== "undefined"`).
- Texte über `svelte-i18n` (`$_`), Styling Tailwind v4 (`bg-linear-to-*`, `aspect-video`, wichtiges `!` hinten: `bg-brand-600!`).
3. Fall in `BlockRenderer.svelte` ergänzen:
```svelte title="BlockRenderer.svelte — neuen Fall einhängen"
{:else if block.type === "quote"}
<QuoteBlock {block} />
```
4. Seite mit dem neuen Block anlegen und anonym prüfen (nächster Abschnitt).
</Steps>
## SSR-Bundle
Der Server ruft `app.default.render({ url })` auf. Den Wrapper liefert `frontend/src/ssr.ts` (Svelte-5-`render` aus `svelte/server`, i18n-Nachrichten synchron geladen, Sprache aus der URL).
```text title="SSR-Bundle: bauen, nie von Hand anfassen"
frontend/src/ssr.ts ── esbuild (esbuild.config.server.js) ──▶ api/hooks/lib/app.server.js
Svelte 5, generate: "server" gebaut, nie von Hand anfassen
```
| Befehl | Wirkung |
|---|---|
| `yarn build` | Frontend-Bundle (`frontend/dist`) |
| `yarn build:server` | SSR-Bundle nach `api/hooks/lib/app.server.js` (Ziel `esnext`, goja-kompatibel) |
Beide Builds schreiben `buildInfo` — der Server-Build überschreibt sie nie, sonst löst `checkBuildVersion` einen Schein-Reload aus.
Nach jedem Frontend-Build: `yarn build:server` erneut laufen lassen und deployen (siehe [Veröffentlichen](/projekt/deploy/)).
## Cache und Invalidierung
`api/hooks/ssr/get_read.js` rendert jede anonyme Anfrage — außer Tracking (`x-ssr-skip`) und Admins:
<Steps>
1. URL normieren (Query ab, Schrägstrich ab, `/index` → `/`), per `ssrValidatePath` prüfen (404 oder kein Rendering).
2. Cache in der Sammlung `ssr` suchen (`path` ist eindeutig). Treffer mit `X-SSR-Cache: true` ausliefern; abgelaufene (`validUntil`) löschen.
3. Neu rendern, `window.__SSR_CACHE__` mit den gelesenen Einträgen einbetten, Abhängigkeiten als `col:id` (Detail) oder `col:*` (Liste) speichern. Gibt es `col:*`, fallen die `col:id` derselben Sammlung weg.
4. Parallel geschriebene Duplikate (`duplicate key` auf `path`) werden geschluckt — der Eintrag existiert dann schon.
</Steps>
Invalidierung übernimmt `api/hooks/lib/utils.js` (`clearSSRCache`), verdrahtet über `clear_cache.js` als `return`-Hook (u. a. `content`, `navigation`, `medialib`, `comment`, `tag`):
| Methode | Was gelöscht wird |
|---|---|
| `POST` (neuer Eintrag) | Nur Seiten mit `col:*` (Listen) |
| `PUT` / `DELETE` | Seiten mit `col:<id>` **oder** `col:*` |
| `POST /api/ssr?clear=1` | Alles (manueller Komplett-Wisch) |
Jede andere `POST`-Anfrage an `ssr` antwortet **500** (`ssr is only a dummy collection`).
Antwort-Köpfe `X-SSR-Cleared` und `X-SSR-Cleared-Collection` sagen, was flog.
## Abnahme
<Tabs>
<TabItem label="Anonym prüfen">
```bash title="SSR-HTML ohne Login lesen"
curl -s http://localhost:8080/de/ | grep -o 'window.__SSR_CACHE__ = .*' | head -c 600
# Erwartung: navigation count > 0, heroImage/image mit _lookup
curl -sI http://localhost:8080/de/ | grep -i x-ssr-cache
# Zweiter Aufruf: X-SSR-Cache: true
```
</TabItem>
<TabItem label="Nach Änderung">
```bash title="Cache-Verhalten nach PUT/DELETE"
# PUT/DELETE auf einen Eintrag → betroffene Seiten neu gerendert (kein X-SSR-Cache: true beim nächsten Aufruf)
# POST (neu) → nur Listen-Seiten neu
# Zweifel? POST /api/ssr?clear=1 wischt alles
curl -s "http://localhost:8080/de/?noCache=1" | head -c 300
```
</TabItem>
</Tabs>
| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Öffentlich leer, mit Token voll | `active:true` fehlt (auch auf Navigation/Medien) | Flag setzen, anonym erneut prüfen |
| `X-SSR-Cache: true` trotz neuer Daten | Abhängigkeit fehlt (z. B. Liste ohne `col:*`) | `?clear=1` oder `?noCache=1`, Hook-Verdrahtung prüfen |
| 500 `ssr is only a dummy collection` | `POST` an `ssr` ohne `?clear=1` | Query-Parameter ergänzen |
| PAT reicht, anonym fehlt Lookup | `lookup permission denied` — Ziel-Sammlung ohne Leserecht | [Rechte und Tokens](/projekt/rechte-tokens/) |
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter).
+103
View File
@@ -0,0 +1,103 @@
---
title: 'Suche'
description: 'Textindex, search-Blöcke, Hybrid-Suche, Neuaufbau des Index aus Projektsicht.'
---
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
Die Suche läuft über `search`-Blöcke in der Sammlungs-YAML plus Abfrage per `q` und `qName`.
Suche-Begriffe: Volltextsuche, Hybrid-Suche, Suchindex neu aufbauen.
## Index anlegen
`content.yml` definiert drei Indexe:
| Index | Schlüssel | Zweck |
|---|---|---|
| `content_text` | `$text:$**`, `defaultLanguage: none` | Volltext über alle Felder |
| `path_lang` | `path` + `lang`, unique | Pfad je Sprache eindeutig |
| `translation` | `translationKey` + `lang`, unique | Übersetzung je Sprache eindeutig |
`navigation.yml` und `medialib.yml` nutzen nur einen `default`-Index im Modus `regex` (kein Textindex nötig).
## search-Blöcke
```yaml title="content.yml — search-Blöcke (gekürzt)"
# content.yml — search (gekürzt)
search:
- name: combined # Hybrid aus semantic + fuzzy
mode: combined
rrf: { k: 60, topK: 100, weights: { semantic: 1.5, fuzzy: 1.0 } }
- name: fuzzy
mode: ngram
autoRegenerate: true
fields: [name, path, meta.title, meta.description, blocks.headline, blocks.subline,
blocks.text, blocks.items.title, blocks.items.text, blocks.items.question, blocks.items.answer]
- name: semantic
mode: vector
autoRegenerate: true
fields: [name, meta.title, meta.description, blocks.headline, blocks.subline,
blocks.text, blocks.items.title, blocks.items.text, blocks.items.question, blocks.items.answer]
vector: { provider: bge_m3 }
```
| `qName` | Sammlung | Modus |
|---|---|---|
| `combined` | `content` | Hybrid (RRF aus `semantic` + `fuzzy`) |
| `fuzzy` | `content` | n-gram, fehlertolerant |
| `semantic` | `content` | Vektor (`bge_m3`) |
| `default` | `tag`, `navigation`, `comment`, `medialib` | regex |
## Lesen mit q
Grundlagen (Filter, Lookup, Facetten) stehen unter [Lesen und suchen](/projekt/abfragen-lesen/). Hier nur die Suche:
```text title="Suche per q und qName"
GET …/content?q=kontakt&qName=combined
GET …/tag?q=hero
GET …/content?q=kontakt&qName=combined&qExplain=1
```
- `q` ist der Suchtext, `qName` wählt den Block (ohne `qName` greift der Standard).
- `qExplain=1` zeigt die Gewichtung — nützlich beim Debuggen.
- Suche mit und ohne `lookup` testen: `?q=…&qName=combined&lookup=blocks.heroImage.image:medialib,blocks.image:medialib` löst Treffer-Bilder gleich auf.
## Hybrid-Kurzüberblick
`combined` mischt Vektor- und n-gram-Treffer per Reciprocal Rank Fusion (`internal/rankfusion`, tibi-server):
```text title="RRF-Formel (combined)"
score(doc) += weight / (k + rank) # k = 60, weights: semantic 1.5, fuzzy 1.0
```
- `topK: 100` begrenzt die Kandidaten je Quelle **und** kappt das Fusionsergebnis (`MergeRRF`).
- Weitere Strategien (`score_rrf`, `score_sum`, `rank_score`, `hybrid`), Normierung (`minmax`, `zscore`), Gewichte (`scoreWeight`) und Quellen-Config (`sourceConfig`) existieren im Code **und** sind als `rrf`-Block in der Projekt-YAML einstellbar — aus Projektsicht reicht `combined` mit den Starter-Gewichten.
- Alte Score-Beispiele aus Erzähl-Docs nicht übernehmen — nur diese Formel gilt.
## Neuaufbau
<Steps>
1. Bearbeite Einträge normal — `fuzzy` und `semantic` haben `autoRegenerate: true` und bauen sich selbst nach.
2. Beobachte im tibi-admin den Hinweis `SearchIndexBuildNotice`: Er erscheint im Projekt-Header (Summe offener `_queue`-Einträge aller `ngram`-/`vector`-Sammlungen) und im Sammlungs-Header der betroffenen Sammlung.
3. Warte, bis der Hinweis nach dem nächsten Polling von selbst verschwindet (`_queue` = 0). Der `regex`-Modus (`default`) hat keine Queue und keinen Hinweis.
</Steps>
<Aside type="note" title="Regenerate auslösen">
Den Neuaufbau stößt du im tibi-admin über den Regenerate-Flow an (Test-Plan `41-search-index-status`).
Aus Projektsicht musst du nichts einrichten — nur `autoRegenerate: true` in der YAML stehen lassen.
</Aside>
## Fehlerbilder
| Symptom | Ursache | Lösung |
|---|---|---|
| Leere Scores / keine Treffer trotz Daten | Index fehlt oder `_queue` noch > 0 | Hinweis im Admin abwarten, dann erneut suchen |
| `combined` findet nichts, `fuzzy` schon | Vektor-Anteil ohne Embeddings | `semantic`-Felder und Provider (`bge_m3`) prüfen |
| Suche über `tag`/`navigation` unscharf | `default` ist `regex`, kein Fuzzy | Erwartung anpassen oder `content`-Indexe nutzen |
| `?facet=` an der Suche wirkungslos | Facetten sind eigene Route (`…/_facet`) | [Lesen und suchen](/projekt/abfragen-lesen/) beachten |
Weiter: [Lesen und suchen](/projekt/abfragen-lesen/), [Sammlungen anlegen](/projekt/sammlungen-anlegen/), [Felder und Widgets](/projekt/felder-widgets/).
Verifiziert am e3f92ebf 2026-10-14 (tibi-svelte-starter), am 68b42785 2026-10-15 (tibi-server, rankfusion), am 2b091d89 2026-10-13 (tibi-admin-nova, Test-Plan 41-search-index-status)
+54
View File
@@ -0,0 +1,54 @@
---
title: 'Fehlerbilder'
description: 'Typische Antworten und was sie bedeuten: project not found, permission denied, 409, Validierung.'
---
import { Aside } from '@astrojs/starlight/components';
Jede Antwort folgt dem Muster `{"error": "<text>"}`. Status plus Text verraten dir Ursache und Fix. Alle Texte unten stehen wörtlich so im Code (`internal/controller/*.go`, `validator.go`). Das gilt für API-Antworten — wirfst du selbst im Hook, steuert `msg` den Fehlertext und `json` den Körper (ohne `json` echot der Server dein geworfenes Objekt; siehe [Hooks schreiben](/projekt/hooks-schreiben/)).
## Tabelle: Fehler zu Ursache zu Fix
| Status | Antwort | Ursache | Fix |
| --- | --- | --- | --- |
| 404 | `project not found` | Projekt läuft auf diesem Server nicht (nicht registriert oder falscher Namespace) | Projekt per JWT registrieren, dann Reload (siehe Token-Köpfe) |
| 404 | `document not found` / `not found` | Datensatz-ID existiert nicht in dieser Collection | ID und Collection-Namen prüfen |
| 403 | `permission denied` | Kopf fehlt, Wert falsch oder Methode nicht erlaubt | Header-Namen, Token-Wert und `permissions`-Block prüfen |
| 403 | `permission denied: …` | Detail steht nach dem Doppelpunkt, z. B. fehlender Scope oder Bulk-Recht | Text nach dem Doppelpunkt lesen: Scope nachtragen oder Einzel- statt Bulk-Route nutzen |
| 403 | `aggregate permission denied` / `lookup permission denied` | Aggregation oder Lookup ohne Recht | `permissions`-Eintrag für diese Operation ergänzen |
| 403 | `permission denied for given projection` | Angeforderte Projektion nicht erlaubt | Projektion aus der Anfrage entfernen oder freigeben |
| 409 / 500 | Doppelter Schlüssel (Konflikt) | Eindeutiger Index verletzt, z. B. `path+lang` doppelt. **Achtung Default:** Standard ist Legacy-Modus (`legacy.httpCodes`), dort antwortet der Server **500** statt 409; 409 gibt es nur im modernen Modus | Eindeutiges Feld ändern; bei 500 denselben Fall annehmen |
| 400 / 422 | `validation failed` + Feldliste | Datensatz verstößt gegen Feld-Validatoren. Legacy-Default: **400**; modern: **422** | `code` je Feld aus der zweiten Tabelle lesen |
| 400 | `invalid data type for data in return of … hook` | Hook gab falschen Typ zurück | Hook-Rückgabe auf Objekt bzw. erwarteten Typ fixen |
## Validierungs-Codes je Feld
Der Server liefert zu jedem Feld `{"code": "…", "message": "…"}`. Alle Codes aus `validator.go`:
| Code | Bedeutung | Typisches Beispiel |
| --- | --- | --- |
| `required` | Pflichtfeld fehlt oder leer | Leerer Titel bei Pflichtfeld |
| `type` | Falscher Datentyp | Array statt String; Zahl statt Bool |
| `format` | Format falsch | `email`, `url`, `uuid`, `slug` verletzt |
| `pattern` | Regulärer Ausdruck passt nicht | `validator.pattern` greift nicht |
| `min_length` / `max_length` | String zu kurz oder zu lang | Titel über dem Limit |
| `min` / `max` | Zahl zu klein oder zu groß | Preis unter Minimum |
| `min_date` / `max_date` | Datum außerhalb des Fensters | Datum vor `minDate` |
| `min_items` / `max_items` | Array hat zu wenige oder zu viele Einträge | Leere Liste bei `minItems: 1` |
| `in` | Wert nicht in der Allow-Liste | Status nicht in `validator.in` |
| `eval` | Eigene JS-Prüfung schlägt fehl | `message` enthält den Hook-Text |
| `unknown_field` | Feld steht nicht im Schema (nur bei `strictFields`) | Tippfehler im Feldnamen |
| `internal` | Validator selbst defekt, z. B. `eval` nicht kompiliert | Config und Server-Logs prüfen |
<Aside type="caution" title="Legacy ist Default">
Ohne `legacy.httpCodes: false` in der Projekt-Config gelten die alten Codes: Konflikt = **500** (nicht 409), Validierung = **400** (nicht 422). Wenn du 500 siehst, wo du 409 erwartest, ist das kein Bug — das ist der Default (`internal/controller/http_status.go`).
</Aside>
```text title="Fehlerbild einordnen"
400/422 validation failed ──▶ code lesen ──▶ Feld fixen ──▶ erneut senden
403 permission denied ──▶ Doppelpunkt-Text lesen ──▶ Kopf oder Recht fixen
404 project not found ──▶ registrieren + Reload ──▶ erneut senden
409/500 Konflikt ──▶ eindeutiges Feld ändern ──▶ erneut senden
```
Verifiziert am 68b42785 2026-10-15 (tibi-server)
@@ -0,0 +1,75 @@
---
title: 'Hook-Kontexte (Code-Stand)'
description: 'Verbindliche Hook-Kontext-Liste aus internal/hook: alle Pakete und wofür sie gut sind.'
---
import { Aside, Card, CardGrid } from '@astrojs/starlight/components';
In Hooks rufst du Helfer über Pakete auf, z. B. `db.find(...)` oder `http.fetch(...)`. Diese Liste stammt aus `internal/hook/context_*.go` im tibi-server (jeweils `RegisterPackageFunctions`). Sie ist verbindlich — nicht die Paket-Zahl aus älteren Erzähl-Docs.
Die Detail-Doku wächst kapitelweise: Jedes Paket verlinkt unten auf sein Referenz-Kapitel.
<CardGrid stagger>
<Card title="Daten">
db, cache, request, response, fs. [Lesen](/reference/hooks-daten/).
</Card>
<Card title="Netze und Nachrichten">
http, smtp, channel, ratelimit. [Lesen](/reference/hooks-netze/).
</Card>
<Card title="Sicherheit und Identität">
jwt, user, bcrypt, crypto, cookie, exec, config. [Lesen](/reference/hooks-sicherheit/).
</Card>
<Card title="Templates und Medien">
tpl, image, pdf. [Lesen](/reference/hooks-medien/).
</Card>
<Card title="Formate und Diagnose">
base64, json, xml, charset, debug. [Lesen](/reference/hooks-meta/).
</Card>
</CardGrid>
## Tiefenstufen: was „abgedeckt" heißt
`npm run coverage` misst pro Symbol (`db.find`, `cache.get`, `request` …) die Tiefe auf seiner Map-Seite: Stufe 1 = Symbol fehlt, Stufe 2 = nur Erwähnung, Stufe 3 = Erwähnung plus Codeblock mit dem Symbol (Start höchstens 30 Zeilen danach), Stufe 4 = zusätzlich ein Fehler-Token (Error-Text oder Status-Code) im selben `##`-Abschnitt. Ziel pro Kapitel: alle eigenen Symbole auf Stufe 4.
## Tabelle: Kontext zu Wofür
| Paket | Wofür | Beispiel | Kapitel |
| --- | --- | --- | --- |
| `db` | Lesen und schreiben in Collections | `db.find`, `db.create`, `db.update`, `db.delete`, `db.count`, `db.updateMany`, `db.deleteMany` | [Hook-Referenz: Daten](/reference/hooks-daten/) |
| `http` | Externe HTTP-Aufrufe | `http.fetch`, `http.fetchStream` | [Hook-Referenz: Netze](/reference/hooks-netze/) |
| `tpl` | Templates rendern | `tpl.execute` | [Hook-Referenz: Templates und Medien](/reference/hooks-medien/) |
| `jwt` | Tokens erzeugen und parsen | `jwt.create`, `jwt.parse` | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `user` | Aktuelle Anmeldung lesen | `user.auth` | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `smtp` | E-Mails senden | `smtp.sendMail` | [Hook-Referenz: Netze](/reference/hooks-netze/) |
| `exec` | Systemkommando ausführen | `exec.command` | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `config` | Projekt-, Namespace-, Server-Config lesen | `config.namespace`, `config.project`, `config.server` | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `request` | Aktuellen Request lesen (kein Paket, sondern Funktion) | `request()` → Methode, URL, Header | [Hook-Referenz: Daten](/reference/hooks-daten/) |
| `response` | Antwort steuern (Header, SSE) | `response.header`, `response.write`, `response.writeSSE` | [Hook-Referenz: Daten](/reference/hooks-daten/) |
| `fs` | Dateien im Config-Verzeichnis | `fs.readFile`, `fs.writeFile`, `fs.stat`, `fs.readDir`, `fs.mkDir`, `fs.remove`, `fs.copy`, `fs.configDir` | [Hook-Referenz: Daten](/reference/hooks-daten/) |
| `image` | Bildfilter anwenden | `image.filter` | [Hook-Referenz: Templates und Medien](/reference/hooks-medien/) |
| `pdf` | PDF erzeugen | `pdf.fromHTML`, `pdf.cpu` | [Hook-Referenz: Templates und Medien](/reference/hooks-medien/) |
| `cache` | Hook-lokaler Zwischenspeicher | `cache.set`, `cache.get`, `cache.has`, `cache.delete`, `cache.clear`, `cache.count` | [Hook-Referenz: Daten](/reference/hooks-daten/) |
| `crypto` | Hashes bilden | `crypto.sha256`, `crypto.md5` (plus weitere) | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `base64` | Kodieren und Dekodieren | `base64.encode`, `base64.decode` | [Hook-Referenz: Formate und Diagnose](/reference/hooks-meta/) |
| `bcrypt` | Passwörter hashen und prüfen | `bcrypt.hash`, `bcrypt.check` | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `channel` | Nachrichten senden und abonnieren | `channel.send`, `channel.subscribe` | [Hook-Referenz: Netze](/reference/hooks-netze/) |
| `cookie` | Cookies lesen und setzen | `cookie.get`, `cookie.set` | [Hook-Referenz: Sicherheit und Identität](/reference/hooks-sicherheit/) |
| `json` | JSON-Helfer | siehe `context_json.go` | [Hook-Referenz: Formate und Diagnose](/reference/hooks-meta/) |
| `xml` | XML-Helfer | siehe `context_xml.go` | [Hook-Referenz: Formate und Diagnose](/reference/hooks-meta/) |
| `charset` | Zeichensätze umwandeln | `charset`-Funktionen, z. B. ISO-8859 nach UTF-8 | [Hook-Referenz: Formate und Diagnose](/reference/hooks-meta/) |
| `debug` | Debug-Ausgaben, Sleep, Trace-ID | `debug.dump`, `debug.sleep`, `debug.sentryTraceId` | [Hook-Referenz: Formate und Diagnose](/reference/hooks-meta/) |
| `ratelimit` | Aufrufe begrenzen | siehe `context_ratelimit.go` | [Hook-Referenz: Netze](/reference/hooks-netze/) |
## Fehlende oder veraltete Einträge melden
Die Hülle nannte nur: db, http, tpl, jwt, bcrypt, cache, channel, crypto, fs, image, pdf, smtp, exec, config, request, response, user, xml, base64. Im Code gibt es **zusätzlich**:
- `charset`, `cookie`, `json`, `debug`, `ratelimit`
Wenn du eine Doku-Stelle findest, die eine kleinere Paket-Zahl nennt, ist sie veraltet. Melde sie als Issue mit dem Dateinamen und dem Stand dieser Seite.
<Aside type="tip" title="Schnell nachschlagen">
Paketname = Dateiname ohne `context_` und ohne `.go`. Die Funktionen stehen jeweils im `RegisterPackageFunctions`-Block am Dateianfang.
</Aside>
Verifiziert am 68b42785 2026-10-15 (tibi-server)
+413
View File
@@ -0,0 +1,413 @@
---
title: 'Hook-Referenz: Daten'
description: 'db, cache, request, response und fs im Hook-JS: Signaturen aus internal/hook, je ein Beispiel und je ein belegter Fehlerfall.'
---
In Hooks sprichst du diese fünf Pakete über `context` an, z. B. `context.db.find(...)` oder `context.request()`. Die Signaturen stammen aus den `// args:`- bzw. `// JS signature:`-Kommentaren in `internal/hook/context_*.go` im tibi-server; wo so ein Kommentar fehlt, ist die Signatur aus Funktionsrumpf und `_test`-Datei abgeleitet und entsprechend markiert. Jede Funktion zeigt genau einen Fehlerfall mit dem originalen Error-Text aus Code oder Tests (Status-Code aus den Hook-Tests, meist `500`).
## db – Collections lesen und schreiben
Quelle: `context_db.go`, registriert per `RegisterPackageFunctions("db", ...)` mit `find`, `count`, `create`, `update`, `delete`, `deleteMany`, `updateMany`.
### db.find
**Signatur:** `db.find(collectionName: string, options?: object): Array` — aus `// args: collectionName:string, options:map[string]interface{}` (`context_db.go:42`). `options` kennt `filter`, `selector` (oder `projection`), `offset`, `limit` und `sort`.
Leeres Ergebnis ist kein Fehler: `db.find` liefert dann ein leeres Array.
```js title="db.find – Beispiel"
;(function () {
const rows = context.db.find("content", {
filter: { active: true },
sort: ["-publishAt"],
limit: 10
})
throw { status: 200, json: { count: rows.length, rows: rows } }
})()
```
**Fehlerfall (Error):** Ohne Collection-Namen brichst du mit `missing collection name argument in db.find` ab (`context_db.go:53`); eine unbekannte Collection liefert den Config-Error und endet im Hook-Test mit Status `500` (`error - find invalid collection`, `context_db_test.go:228`).
### db.count
**Signatur:** `db.count(collectionName: string, options?: { filter?: object }): number` — aus `// args: collectionName:string, options:map[string]interface{}` (`context_db.go:186`).
```js title="db.count – Beispiel"
;(function () {
const n = context.db.count("content", { filter: { active: true } })
throw { status: 200, json: { count: n } }
})()
```
**Fehlerfall (Error):** Ein `filter` vom falschen Typ (z. B. String statt Objekt) scheitert mit `invalid type for options.filter in db.count` (`context_db.go:222`).
### db.create
**Signatur:** `db.create(collectionName: string, data: object): object` — aus `// args: collectionName:string, data:map[string]interface{}` (`context_db.go:260`). Gibt das gespeicherte Dokument inklusive `id` zurück.
```js title="db.create – Beispiel"
;(function () {
const doc = context.db.create("content", { title: "Neu", active: false })
throw { status: 200, json: { id: doc.id, title: doc.title } }
})()
```
**Fehlerfall (Error):** Fehlende `data` melden die Tests belegt: `missing collection name and/or data argument in db.create`, Status `500` (`error - create missing data`, `context_db_test.go:239`).
### db.update
**Signatur:** `db.update(collectionName: string, id: string, data: object, options?: { raw?: boolean }): object` — aus `// args: collectionName:string, id:string(ObjectID hex), data:map[string]interface{}` (`context_db.go:313`). `data` versteht Feld-Operatoren wie `{"price": {"$inc": 10}}`; mit `{ raw: true }` übergibst du ein rohes Mongo-Update-Dokument.
```js title="db.update – Beispiel"
;(function () {
const id = context.request().query("id")
const doc = context.db.update("content", id, { title: "Korrigiert" })
throw { status: 200, json: { id: doc.id, title: doc.title } }
})()
```
**Fehlerfall (Error):** Eine ID, die kein ObjectID-Hex ist, liefert `invalid object id string in db.update` (`context_db.go:365`).
### db.delete
**Signatur:** `db.delete(collectionName: string, id: string): object` — aus `// args: collectionName:string, id:string(ObjectId Hex)` (`context_db.go:453`). Gibt `{ message: "ok" }` zurück und räumt Datei-Felder des Dokuments weg.
```js title="db.delete – Beispiel"
;(function () {
const id = context.request().query("id")
const res = context.db.delete("content", id)
throw { status: 200, json: { deleted: id, message: res.message } }
})()
```
**Fehlerfall (Error):** Ungültige ID: `invalid object id string in db.delete` (`context_db.go:479`); fehlende Argumente: `missing collection name and/or id in db.delete`.
### db.deleteMany
**Signatur:** `db.deleteMany(collectionName: string, options?: { filter?: object }): object` — aus `// args: collectionName:string, options:map[string]interface{}` (`context_db.go:533`). Antwortet mit `{ message: "ok", removed: <Anzahl> }`.
```js title="db.deleteMany – Beispiel"
;(function () {
const res = context.db.deleteMany("content", { filter: { active: false } })
throw { status: 200, json: { removed: res.removed } }
})()
```
**Fehlerfall (Error):** `options` als String statt Objekt scheitert mit `options must be object in db.deleteMany` (`context_db.go:552`).
### db.updateMany
**Signatur:** `db.updateMany(collectionName: string, options: { filter?: object, changes?: object, update?: object }): object` — aus `// args: collectionName:string, options:map[string]interface{}` (`context_db.go:628`). Entweder `changes` (einfache Werte, werden `$set`) oder `update` (rohe Operatoren wie `$inc`, `$unset`) ist Pflicht; `update` gewinnt bei beiden.
```js title="db.updateMany – Beispiel"
;(function () {
const res = context.db.updateMany("content", {
filter: { active: false },
changes: { reviewed: true }
})
throw { status: 200, json: { modified: res.modified } }
})()
```
**Fehlerfall (Error):** Ohne `changes` und ohne `update`: `options.changes or options.update is required in db.updateMany` (`context_db.go:711`).
## cache – Hook-lokaler Zwischenspeicher
Quelle: `context_cache.go`, registriert per `RegisterPackageFunctions("cache", ...)` mit `set`, `get`, `has`, `delete`, `clear`, `count`. Der Cache ist projektweit (project-scoped), nicht pro Request.
### cache.set
**Signatur:** `cache.set(key: string, value: any, ttlMs?: number): void` — aus `// JS signature: cache.set(key: string, value: any, ttlMs?: number): void` (`context_cache.go:23`).
```js title="cache.set – Beispiel"
;(function () {
context.cache.set("content:teaser", { title: "Start" }, 60000)
throw { status: 200, json: { cached: true } }
})()
```
**Fehlerfall (Error):** Weniger als zwei Argumente: `cache.set requires at least key and value` (`context_cache.go:30`); leerer Schlüssel: `cache.set: key must be a non-empty string` (`context_cache.go:34`).
### cache.get
**Signatur:** `cache.get(key: string): any | null` — aus `// JS signature: cache.get(key: string): any | null` (`context_cache.go:48`). Unbekannte Schlüssel liefern `null`, keinen Error.
```js title="cache.get – Beispiel"
;(function () {
const teaser = context.cache.get("content:teaser")
throw { status: 200, json: { hit: teaser !== null, teaser: teaser } }
})()
```
**Fehlerfall (Error):** Aufruf ohne Schlüssel: `cache.get requires a key` (`context_cache.go:55`).
### cache.has
**Signatur:** `cache.has(key: string): boolean` — aus `// JS signature: cache.has(key: string): boolean` (`context_cache.go:70`).
```js title="cache.has – Beispiel"
;(function () {
const hit = context.cache.has("content:teaser")
throw { status: 200, json: { hit: hit } }
})()
```
**Fehlerfall (Error):** Aufruf ohne Schlüssel: `cache.has requires a key` (`context_cache.go:77`).
### cache.delete
**Signatur:** `cache.delete(key: string): boolean` — aus `// JS signature: cache.delete(key: string): boolean` (`context_cache.go:88`). Gibt zurück, ob der Schlüssel existierte.
```js title="cache.delete – Beispiel"
;(function () {
const gone = context.cache.delete("content:teaser")
throw { status: 200, json: { deleted: gone } }
})()
```
**Fehlerfall (Error):** Aufruf ohne Schlüssel: `cache.delete requires a key` (`context_cache.go:95`).
### cache.clear
**Signatur:** `cache.clear(): void` — aus `// JS signature: cache.clear(): void` (`context_cache.go:106`). Räumt alle Einträge des Projekt-Caches weg.
```js title="cache.clear – Beispiel"
;(function () {
context.cache.clear()
throw { status: 200, json: { cleared: true } }
})()
```
**Fehlerfall (Error):** Ohne initialisierten Projekt-Cache meldet der Code `cache.clear: cache not available` (`context_cache.go:110`).
### cache.count
**Signatur:** `cache.count(): number` — aus `// JS signature: cache.count(): number` (`context_cache.go:118`). Zählt nur lebende (nicht abgelaufene) Einträge.
```js title="cache.count – Beispiel"
;(function () {
const n = context.cache.count()
throw { status: 200, json: { entries: n } }
})()
```
**Fehlerfall (Error):** Ohne initialisierten Projekt-Cache meldet der Code `cache.count: cache not available` (`context_cache.go:122`).
## request – Aktuellen Request lesen
`request` ist kein Paket, sondern eine direkt registrierte Funktion (`Register("request", ...)` in `context_request.go:12`). **Signatur aus Code und Tests abgeleitet** (kein `args:`-Kommentar, `context_request.go:15`): `request(): { method, remoteAddr, clientIp, host, url, path, param(), query(), header(), bodyBytes(), body() }`. `method` ist kleingeschrieben, `query`, `header` und `param` sind Funktionen.
### request
**Signatur (abgeleitet):** `request(): object` — Felder und Helfer siehe oben, belegt durch `FuncRequest` (`context_request.go:16`) und `test_request.js` (Methode, Pfad, Query, Header, Host, URL, Remote-Adresse).
```js title="request – Beispiel"
;(function () {
const req = context.request()
const out = {
method: req.method,
path: req.path,
filter: req.query("filter"),
token: req.header("X-Custom-Test")
}
throw { status: 200, json: out }
})()
```
**Fehlerfall (Error):** Außerhalb von Hooks (kein HTTP-Kontext, z. B. in Jobs ohne Request) scheitert der Aufruf mit `request functions only valid in hooks` (`context_request.go:19`).
## response – Antwort steuern
Quelle: `context_response.go`, registriert per `RegisterPackageFunctions("response", ...)` mit `header`, `writeHeader`, `write`, `flush`, `writeSSE`. Alle Funktionen brauchen einen Hook-HTTP-Kontext. Muster: `test_response.js` im tibi-server.
### response.header
**Signatur aus Kommentar** (`context_response.go:21`, `// header:string content:string`): `response.header(name: string, content: string): void` — setzt einen Antwort-Header.
```js title="response.header – Beispiel"
;(function () {
context.response.header("X-SSR-Cache", "hit")
context.response.header("Cache-Control", "public, max-age=60")
throw { status: 200, json: { headers: "set" } }
})()
```
**Fehlerfall (Error):** Weniger als zwei Argumente: `missing header name and/or value in reponse.header function` — so wörtlich im Code, inklusive Tippfehler (`context_response.go:29`).
### response.writeHeader
**Signatur:** `response.writeHeader(status: number): void` — aus `// args: status:int` (`context_response.go:43`). Muss vor `write` kommen; belegt durch `write_header_and_body` in `test_response.js` (Status `201`).
```js title="response.writeHeader – Beispiel"
;(function () {
context.response.header("Content-Type", "text/plain")
context.response.writeHeader(201)
context.response.write("Angelegt")
return { stop: true }
})()
```
**Fehlerfall (Error):** Nicht-numerischer Status: `status must be a number in response.writeHeader function` (`context_response.go:61`); ohne `writeHeader` sendet das erste `write` implizit Status `200` (`context_response.go:68`).
### response.write
**Signatur:** `response.write(data: string | []byte): void` — aus `// args: data:string|[]byte` (`context_response.go:70`). Schreibt direkt in den Antwort-Body.
```js title="response.write – Beispiel"
;(function () {
context.response.header("Content-Type", "application/json")
context.response.writeHeader(200)
context.response.write(JSON.stringify({ custom: true }))
return { stop: true }
})()
```
**Fehlerfall (Error):** Aufruf ohne Daten: `missing data in response.write function` (`context_response.go:78`).
### response.flush
**Signatur ohne `args:`-Kommentar, aus Rumpf und Tests abgeleitet** (`context_response.go:101`): `response.flush(): void` — schiebt gepufferte Daten sofort zum Client, wichtig für Streaming.
```js title="response.flush – Beispiel"
;(function () {
context.response.header("Content-Type", "text/plain")
context.response.writeHeader(200)
context.response.write("part1")
context.response.flush()
context.response.write("part2")
return { stop: true }
})()
```
**Fehlerfall (Error):** Außerhalb von Hooks (ohne HTTP-Kontext) scheitert der Aufruf mit `response functions only valid in hooks` (`context_response.go:104`); kann der Writer nicht flushen, ist `flush` still wirkungslos.
### response.writeSSE
**Signatur:** `response.writeSSE(event: string, data: string): void` — aus `// args: event:string, data:string` (`context_response.go:115`). Schreibt `event: {event}\ndata: {data}\n\n` und flusht sofort; mit nur einem Argument wird es als `data` ohne Event-Name gesendet.
```js title="response.writeSSE – Beispiel"
;(function () {
context.response.header("Content-Type", "text/event-stream")
context.response.writeHeader(200)
context.response.writeSSE("tick", "event-data-1")
return { stop: true }
})()
```
**Fehlerfall (Error):** Aufruf ohne Argumente: `missing data in response.writeSSE function` (`context_response.go:129`).
## fs – Dateien im Config-Verzeichnis
Quelle: `context_fs.go`, registriert per `RegisterPackageFunctions("fs", ...)` mit `configDir`, `readFile`, `writeFile`, `stat`, `readDir`, `mkDir`, `remove`, `copy`. Alle Pfade sind relativ und werden per `CleanFilePath` gegen das Config-Verzeichnis aufgelöst. Muster: `test_fs.js` im tibi-server.
### fs.configDir
**Signatur ohne `args:`-Kommentar, aus dem Rumpf abgeleitet** (`context_fs.go:27`): `fs.configDir(): string` — liefert das Config-Verzeichnis, nimmt keine Argumente.
```js title="fs.configDir – Beispiel"
;(function () {
const dir = context.fs.configDir()
throw { status: 200, json: { hasDir: dir !== null && dir !== undefined } }
})()
```
**Fehlerfall:** Kein Fehlerfall — `configDir` nimmt keine Argumente und wirft keinen Pfad-Fehler (dokumentierte Abwesenheit, kein Versäumnis).
### fs.readFile
**Signatur:** `fs.readFile(path: string, options?: { bytes?: boolean }): string` — aus `// args: path:string` (`context_fs.go:31`); mit `{ bytes: true }` kommen Bytes statt String zurück.
```js title="fs.readFile – Beispiel"
;(function () {
const content = context.fs.readFile("snippets/hint.txt")
throw { status: 200, json: { content: content } }
})()
```
**Fehlerfall (Error):** Ohne Pfad: `missing path in fs.readFile function` mit Status `500`; eine fehlende Datei liefert ebenfalls Status `500` mit `$.error` — beides belegt in `context_fs_test.go:109` („error - missing path").
### fs.writeFile
**Signatur:** `fs.writeFile(path: string, data: string | []byte): void` — aus `// args: path:string data:[]bytes|string` (`context_fs.go:79`).
```js title="fs.writeFile – Beispiel"
;(function () {
context.fs.writeFile("tmp/note.txt", "Hallo Hook")
const back = context.fs.readFile("tmp/note.txt")
context.fs.remove("tmp/note.txt")
throw { status: 200, json: { content: back } }
})()
```
**Fehlerfall (Error):** Ohne Daten: `missing data in fs.writeFile function` (`context_fs.go:85`); ein Pfad außerhalb des Config-Verzeichnisses scheitert in `CleanFilePath` mit einem Error `... in fs.writeFile`.
### fs.stat
**Signatur:** `fs.stat(path: string): object` — aus `// args: path:string` (`context_fs.go:120`). Antwortet mit `{ name, isDir, size, modTime }`.
```js title="fs.stat – Beispiel"
;(function () {
const info = context.fs.stat("tmp/note.txt")
throw { status: 200, json: { name: info.name, size: info.size } }
})()
```
**Fehlerfall (Error):** Fehlende Datei: Error `... in fs.stat` aus `os.Stat` (`context_fs.go:147`); ohne Pfad: `missing path in fs.stat function`.
### fs.readDir
**Signatur:** `fs.readDir(path: string): Array` — aus `// args: path:string` (`context_fs.go:158`). Liefert pro Eintrag `{ name, isDir, size, modTime }`.
```js title="fs.readDir – Beispiel"
;(function () {
const files = context.fs.readDir("tmp")
throw { status: 200, json: { count: files.length, files: files } }
})()
```
**Fehlerfall (Error):** Unbekanntes Verzeichnis: Error `... in fs.readDir` aus `os.ReadDir` (`context_fs.go:182`); ohne Pfad: `missing path in fs.readDir function`.
### fs.mkDir
**Signatur:** `fs.mkDir(path: string): void` — aus `// args: path:string` (`context_fs.go:203`). Nutzt `MkdirAll`, legt also auch fehlende Elternverzeichnisse an.
```js title="fs.mkDir – Beispiel"
;(function () {
context.fs.mkDir("tmp/export")
context.fs.writeFile("tmp/export/a.txt", "eins")
throw { status: 200, json: { created: true } }
})()
```
**Fehlerfall (Error):** Ohne Pfad: `missing path in fs.mkDir function` (`context_fs.go:206`).
### fs.remove
**Signatur:** `fs.remove(path: string): void` — aus `// args: path:string` (`context_fs.go:230`).
```js title="fs.remove – Beispiel"
;(function () {
context.fs.writeFile("tmp/alt.txt", "weg")
context.fs.remove("tmp/alt.txt")
throw { status: 200, json: { removed: true } }
})()
```
**Fehlerfall (Error):** Fehlende Datei: Error `... in fs.remove` aus `os.Remove` (`context_fs.go:251`); ohne Pfad: `missing path in fs.remove function`.
### fs.copy
**Signatur:** `fs.copy(src: string, dest: string): void` — aus `// args: src:string dest:string` (`context_fs.go:257`).
```js title="fs.copy – Beispiel"
;(function () {
context.fs.copy("tmp/export/a.txt", "tmp/export/b.txt")
throw { status: 200, json: { copied: true } }
})()
```
**Fehlerfall (Error):** Ohne Quelle: `missing src in fs.copy function`, ohne Ziel: `missing dest in fs.copy function` (`context_fs.go:260`).
Weiter: [Hook-Kontexte](/reference/hook-kontexte/) als Paket-Index, [Hooks schreiben](/projekt/hooks-schreiben/) für Lifecycle und Starter-Muster.
Verifiziert am 68b42785 2026-10-16 (tibi-server)
@@ -0,0 +1,84 @@
---
title: 'Hook-Referenz: Templates und Medien'
description: 'tpl, image und pdf im Hook-JS: Signaturen aus internal/hook, je ein Beispiel und je ein belegter Fehlerfall.'
---
In Hooks sprichst du diese drei Pakete über `context` an, z. B. `context.tpl.execute(...)` oder `context.pdf.fromHTML(...)`. Die Signaturen stammen aus den `// args:`- bzw. `// JS signature:`-Kommentaren in `internal/hook/context_*.go` im tibi-server; wo so ein Kommentar fehlt, ist die Signatur aus Funktionsrumpf und Options-Auswertung abgeleitet und entsprechend markiert. Jede Funktion zeigt genau einen Fehlerfall mit dem originalen Error-Text aus dem Code (Status-Code aus den Hook-Tests, meist `500`).
## tpl – Templates rendern
Quelle: `context_tpl.go`, registriert per `RegisterPackageFunctions("tpl", ...)` mit `execute`. Rendert Pongo2-Templates (Django/Jinja2-Syntax); übergebene Funktionen in `contextData` bleiben aufrufbar (`WrapFunctions`).
### tpl.execute
**Signatur:** `tpl.execute(code: string, contextData?: object): string` — aus `// args: code:string, contextData:map[string]interface{}` (`context_tpl.go:17`). `contextData` ist optional; ohne Daten renderst du statischen Text.
```js title="tpl.execute – Beispiel"
;(function () {
const html = context.tpl.execute("Hallo {{ name }}! Du hast {{ count }} Nachrichten.", {
name: "John",
count: 5
})
throw { status: 200, json: { html: html } }
})()
```
**Fehlerfall (Error):** Aufruf ohne Code brichst du mit `missing code in tpl.execute function` ab (`context_tpl.go:20`); ein `contextData` vom falschen Typ meldet `contextData must be object in tpl.execute function` (`context_tpl.go:34`). Der erste Fall ist durch Tests belegt und endet mit Status `500` (`error - missing code`, `context_tpl_test.go:78`).
## image – Bilder verarbeiten
Quelle: `context_image.go`, registriert per `RegisterPackageFunctions("image", ...)` mit `filter`. Beide Pfade werden per `CleanFilePath` gegen das Config-Verzeichnis aufgelöst. Die Funktion gibt nichts zurück (`nil`).
### image.filter
**Signatur:** `image.filter(sourceFilename: string, targetFilename: string, filters: Array<object>): void` — aus `// args: sourceFilename:string targetFilename:string filters:[]interface{}` (`context_image.go:18`). Jedes Filter-Objekt ist optional bestückt: `width`, `height`, `fit`, `fill`, `resampling`, `anchor`, `brightness`, `saturation`, `contrast`, `gamma`, `blur`, `sharpen`, `invert`, `grayscale`, `quality`, `lossless`, `skipLargerDimension`, `skipLargerFilesize`, `outputType`.
```js title="image.filter – Beispiel"
;(function () {
context.image.filter("uploads/foto.jpg", "uploads/foto-klein.webp", [
{ width: 800, fit: true, quality: 80, outputType: "webp" }
])
throw { status: 200, json: { filtered: true } }
})()
```
**Fehlerfall (Error):** Weniger als drei Argumente: `missing sourceFile, targetFile and filters in image.filter function` (`context_image.go:43`); ein leeres Filter-Array: `filters is empty array in image.filter function` (`context_image.go:73`); ein Filter-Eintrag, der kein Objekt ist: `filter index %d is not an object in image.filter function` (`context_image.go:236`).
## pdf – PDFs erzeugen und bearbeiten
Quelle: `context_pdf.go`, registriert per `RegisterPackageFunctions("pdf", ...)` mit `fromHTML`, `cpu`. Beide Funktionen liefern PDF-Bytes zurück.
### pdf.fromHTML
**Signatur (abgeleitet):** `pdf.fromHTML(html: string, options?: object): bytes` — kein `args:`-Kommentar (nur `// get request object`, `context_pdf.go:28`), abgeleitet aus dem Rumpf (`context_pdf.go:29`): `html` ist Pflicht, `options` optional. `options` kennt unter anderem `orientation` (`Portrait` oder `Landscape`), `marginTop`, `marginBottom`, `marginLeft`, `marginRight`, `dpi`, `title`, `headerHTML`, `footerHTML` und `printMediaType`.
```js title="pdf.fromHTML – Beispiel"
;(function () {
const pdfBytes = context.pdf.fromHTML("<html><body><h1>Report</h1></body></html>", {
orientation: "Portrait",
marginTop: 10,
title: "Report"
})
throw { status: 200, bytes: pdfBytes }
})()
```
**Fehlerfall (Error):** Aufruf ohne HTML: `missing html in pdf.fromHTML` (`context_pdf.go:31`); ein `orientation`-Wert ungleich `Portrait`/`Landscape`: `orientation is not Portrait or Landscape` (`context_pdf.go:152`).
### pdf.cpu
**Signatur (abgeleitet):** `pdf.cpu(command: string, pdfData: bytes | Array<bytes> | object, options?: object): bytes` — kein `args:`-Kommentar, abgeleitet aus dem Rumpf (`context_pdf.go:249`). Kommandos: `create`, `rotate`, `merge`, `stamp`, `watermark`; `merge` nimmt ein Byte-Array, `create` zusätzlich ein JSON-Objekt als Bauplan. Wichtige `options`: `rotation` (Integer-Literal, für `rotate` — goja übergibt Ganzzahlen als `int64`, Fließkomma wie `90.0` scheitert mit `rotation option must be integer in pdf.cpu`), `pages` (Array), `mode` (`text`, `image` oder `pdf` für `stamp`/`watermark`), `text`, `bytes`, `file`, `description`.
```js title="pdf.cpu – Beispiel"
;(function () {
const base = context.pdf.fromHTML("<html><body><h1>Hi</h1></body></html>")
const rotated = context.pdf.cpu("rotate", base, { rotation: 90 })
throw { status: 200, bytes: rotated }
})()
```
**Fehlerfall (Error):** Aufruf ohne Kommando: `missing command in pdf.cpu` (`context_pdf.go:251`); ohne PDF-Daten: `missing pdf input in pdf.cpu` (`context_pdf.go:254`); ein unbekanntes Kommando meldet `<command> is unkown command in pdf.cpu` — so wörtlich im Code, inklusive Tippfehler (`context_pdf.go:446`).
Weiter: [Hook-Kontexte](/reference/hook-kontexte/) als Paket-Index, [Hooks schreiben](/projekt/hooks-schreiben/) für Lifecycle und Starter-Muster.
Verifiziert am 68b42785 2026-10-16 (tibi-server)
+173
View File
@@ -0,0 +1,173 @@
---
title: 'Hook-Referenz: Formate und Diagnose'
description: 'base64, json, xml, charset und debug im Hook-JS: Signaturen aus internal/hook, je ein Beispiel und je ein belegter Fehlerfall.'
---
In Hooks sprichst du diese fünf Pakete über `context` an, z. B. `context.base64.encode(...)` oder `context.json.parse(...)`. Die Signaturen stammen aus den `// args:`-Kommentaren in `internal/hook/context_*.go` im tibi-server; wo so ein Kommentar fehlt, ist die Signatur aus Funktionsrumpf und `_test`-Datei abgeleitet und entsprechend markiert. Jede Funktion zeigt genau einen Fehlerfall mit dem originalen Error-Text aus Code oder Tests (Status-Code aus den Hook-Tests, meist `500`).
## base64 – Binärdaten als Text
Quelle: `context_base64.go`, registriert per `RegisterPackageFunctions("base64", ...)` mit `encode`, `decode`.
### base64.encode
**Signatur:** `base64.encode(data: string | []byte | []interface{}): string` — aus `// args: data:string|[]byte|[]interface{}` (`context_base64.go:17`). Nimmt einen String, Bytes oder ein JS-Zahlenarray und liefert Standard-Base64.
```js title="base64.encode – Beispiel"
;(function () {
const encoded = context.base64.encode("Hallo Welt")
throw { status: 200, json: { encoded: encoded } }
})()
```
**Fehlerfall (Error):** Ohne Argument brichst du mit `missing data in base64.encode function` ab (`context_base64.go:20`), belegt mit Status `500` (`error - missing data in encode`, `context_base64_test.go:94`).
### base64.decode
**Signatur:** `base64.decode(encoded: string, options?: { returnBytes?: boolean }): string | []byte` — aus `// args: encoded:string, options:map[string]interface{}` mit `returnBytes:bool` (`context_base64.go:53`). Ohne Option kommt ein String zurück, mit `{ returnBytes: true }` Bytes.
```js title="base64.decode – Beispiel"
;(function () {
const text = context.base64.decode("SGVsbG8gV2VsdA==")
throw { status: 200, json: { text: text } }
})()
```
**Fehlerfall (Error):** Ein ungültiger Base64-String scheitert mit `invalid base64 string in base64.decode function` (`context_base64.go:86`), belegt mit Status `500` (`error - invalid base64 string`, `context_base64_test.go:105`).
## json – Parsen und Erzeugen
Quelle: `context_json.go`, registriert per `RegisterPackageFunctions("json", ...)` mit `parse`, `stringify`.
### json.parse
**Signatur:** `json.parse(str: string): any` — aus `// args: str:string` (`context_json.go:17`). Wandelt einen JSON-String in ein JS-Objekt um.
```js title="json.parse – Beispiel"
;(function () {
const obj = context.json.parse('{"name":"Test","wert":42}')
throw { status: 200, json: { name: obj.name, wert: obj.wert } }
})()
```
**Fehlerfall (Error):** Ohne Argument meldet der Code `invalid number of arguments` (`context_json.go:20`), belegt mit Status `500` (`error - parse missing argument`, `context_json_test.go:104`).
### json.stringify
**Signatur:** `json.stringify(obj: any): string` — aus `// args: obj:any` (`context_json.go:35`). Wandelt ein JS-Objekt in einen JSON-String um.
```js title="json.stringify – Beispiel"
;(function () {
const str = context.json.stringify({ name: "Test", wert: 42 })
throw { status: 200, json: { str: str } }
})()
```
**Fehlerfall (Error):** Ohne Argument meldet der Code `invalid number of arguments` (`context_json.go:38`); ein nicht serialisierbares Objekt reicht den Marshal-Error durch.
## xml – Erzeugen und Parsen
Quelle: `context_xml.go`, registriert per `RegisterPackageFunctions("xml", ...)` mit `create`, `parse`.
### xml.create
**Signatur:** `xml.create(data: object, options?: { rootElement?: string, includeHeader?: boolean }): string` — aus dem `// args:`-Block mit `data: map`, `rootElement: string` und `includeHeader: bool` (`context_xml.go:19`). Mit `includeHeader: true` stellst du `<?xml version="1.0" encoding="UTF-8"?>` voran.
```js title="xml.create – Beispiel"
;(function () {
const out = context.xml.create({ user: { name: "Anna", ort: "Berlin" } })
throw { status: 200, json: { xml: out } }
})()
```
**Fehlerfall (Error):** Ohne Daten brichst du mit `missing data in xml.create function` ab (`context_xml.go:27`), belegt mit Status `500` (`xml creation error - missing data`, `context_xml_test.go:61`).
### xml.parse
**Signatur:** `xml.parse(xml: string, options?: object): object` — aus `// args: xml:string options:map[string]interface{}` (`context_xml.go:110`). Attribute landen unter `-`-Präfix (z. B. `"-id"`).
```js title="xml.parse – Beispiel"
;(function () {
const obj = context.xml.parse("<root><name>Anna</name></root>")
throw { status: 200, json: { name: obj.root.name } }
})()
```
**Fehlerfall (Error):** Ohne Argument brichst du mit `missing xml string in xml.parse function` ab (`context_xml.go:113`), belegt mit Status `500` (`xml parsing error - missing xml`, `context_xml_test.go:190`).
## charset – Fremde Zeichensätze dekodieren
Quelle: `context_charset.go`, registriert per `RegisterPackageFunctions("charset", ...)` mit `decode`, `iso8859ToUtf8`.
### charset.decode
**Signatur:** `charset.decode(input: string | []byte | []int64, encoding: string): string` — aus `// args: input:string|[]byte|[]int64, encoding:string` (`context_charset.go:118`). Versteht `utf-8`, `iso-8859-1`/`latin1` sowie alle Labels aus `htmlindex` (z. B. `windows-1252`, `shift_jis`); UTF-8 mit BOM wird entstückt.
```js title="charset.decode – Beispiel"
;(function () {
const text = context.charset.decode([228, 246, 252], "iso-8859-1")
throw { status: 200, json: { text: text } }
})()
```
**Fehlerfall (Error):** Ungültige UTF-8-Bytes scheitern mit `invalid utf-8 bytes in charset.decode` (Error-String `invalid utf-8 bytes` in `context_charset.go:86`), belegt mit Status `500` (`decode utf8 invalid bytes`, `context_charset_test.go:133`).
### charset.iso8859ToUtf8
**Signatur:** `charset.iso8859ToUtf8(iso8859: string | []byte | []int64): string` — aus `// args: iso8859:string` (`context_charset.go:60`). Kurzform für den häufigsten Altbestand-Fall (entspricht `decode` mit `iso-8859-1`).
```js title="charset.iso8859ToUtf8 – Beispiel"
;(function () {
const text = context.charset.iso8859ToUtf8([72, 228])
throw { status: 200, json: { text: text } }
})()
```
**Fehlerfall (Error):** Ohne Argument brichst du mit `missing iso8859 string in charset.iso8859ToUtf8` ab (`context_charset.go:63`), belegt mit Status `500` (`error missing arg`, `context_charset_test.go:155`).
## debug – Dump, Trace und Pause
Quelle: `context_debug.go`, registriert per `RegisterPackageFunctions("debug", ...)` mit `dump`, `sentryTraceId`, `sleep`.
### debug.dump
**Signatur:** `debug.dump(toDump?: any): void` — aus `// args: toDump:any` (`context_debug.go:21`). Schreibt die Argumente per `spew` auf die Server-Konsole und gibt immer `null` zurück.
```js title="debug.dump – Beispiel"
;(function () {
context.debug.dump({ hinweis: "Stand prüfen", wert: 42 })
throw { status: 200, json: { message: "dump completed" } }
})()
```
**Fehlerfall:** Kein Fehlerfall — `dump` nimmt beliebige (auch keine) Argumente und liefert stets ohne Beanstandung zurück (`context_debug.go:49`, dokumentierte Abwesenheit, kein Versäumnis).
### debug.sentryTraceId
**Signatur (abgeleitet):** `debug.sentryTraceId(): string` — kein `args:`-Kommentar, nur `// get sentry trace_id` (`context_debug.go:52`); aus Rumpf (`FuncDebugSentryTraceId`, `context_debug.go:53`) und Hook-Kontext abgeleitet. Liefert die Sentry-Trace-ID der laufenden Transaktion oder einen leeren String ohne Transaktion.
```js title="debug.sentryTraceId – Beispiel"
;(function () {
const traceId = context.debug.sentryTraceId()
throw { status: 200, json: { traceId: traceId } }
})()
```
**Fehlerfall (Error):** Außerhalb von Hooks (kein Hook-Kontext) scheitert der Aufruf mit `sentry functions are only valid in hooks` (`context_debug.go:55`).
### debug.sleep
**Signatur:** `debug.sleep(duration: int): void` — aus `// args: duration:int` (`context_debug.go:68`). Hält den Hook für `duration` Millisekunden an; nur für Diagnose gedacht.
```js title="debug.sleep – Beispiel"
;(function () {
context.debug.sleep(50)
throw { status: 200, json: { slept: true } }
})()
```
**Fehlerfall (Error):** Ohne genau ein Argument meldet der Code `sleep function requires 1 argument` (`context_debug.go:71`), belegt mit Status `500` (`error - sleep missing argument`, `context_debug_test.go:44`).
Weiter: [Hook-Kontexte](/reference/hook-kontexte/) als Paket-Index, [Hook-Referenz: Daten](/reference/hooks-daten/) für db, cache, request, response und fs.
Verifiziert am 68b42785 2026-10-16 (tibi-server)
+191
View File
@@ -0,0 +1,191 @@
---
title: 'Hook-Referenz: Netze und Nachrichten'
description: 'http, smtp, channel und ratelimit im Hook-JS: Signaturen aus internal/hook, je ein Beispiel und je ein belegter Fehlerfall.'
---
In Hooks sprichst du diese vier Pakete über `context` an, z. B. für einen Fetch-Request oder den Mail-Versand. Die Signaturen stammen aus den `// args:`- bzw. `// JS signature:`-Kommentaren in `internal/hook/context_*.go` im tibi-server; alle vier Pakete sind dort lückenlos kommentiert, keine Signatur musste abgeleitet werden. Jede Funktion zeigt genau einen Fehlerfall mit dem originalen Error-Text aus Code oder Tests.
## http – Externe Requests absetzen
Quelle: `context_http.go`, registriert per `RegisterPackageFunctions("http", ...)` mit `fetch`, `fetchStream`. Beide teilen sich die Argumentprüfung in `parseHTTPArgs`; `fetch` liest den Body vollständig ein (Timeout-Default `10000` ms), `fetchStream` liefert einen zeilenweisen Reader für SSE und Streaming (Timeout-Default `0`, also kein Timeout).
### http.fetch
**Signatur:** `http.fetch(url: string, options?: { method?: string, headers?: object, body?: string, timeout?: number }): object` — aus `// args: url:string, options:map[string]interface{}` (`context_http.go:124`). Antwortet mit `{ headers, status, statusText, trailer, url, body: { text(), json() } }`; `body.json()` wirft bei ungültigem JSON den Parser-Error als JS-Exception.
```js title="http.fetch – Beispiel"
;(function () {
const res = context.http.fetch("https://api.example.com/content", {
method: "GET",
headers: { Accept: "application/json" },
timeout: 5000
})
throw { status: 200, json: { status: res.status, data: res.body.json() } }
})()
```
**Fehlerfall (Error):** Ohne URL brichst du mit `missing url in http.fetch function` ab (`context_http.go:37`); eine URL vom falschen Typ liefert `url must be string in http.fetch function` (`context_http.go:45`), belegt durch die Test-Assertions auf beide Texte (`context_http_test.go:432`, `context_http_test.go:442`).
### http.fetchStream
**Signatur:** `http.fetchStream(url: string, options?: { method?: string, headers?: object, body?: string, timeout?: number }): object` — aus `// args: url:string, options:map[string]interface{}` (`context_http.go:186`). Antwortet mit `{ headers, status, statusText, body: { read(), close() } }`; `read()` liefert die nächste Zeile als String oder `null` bei EOF und schließt dann automatisch.
```js title="http.fetchStream – Beispiel"
;(function () {
const res = context.http.fetchStream("https://api.example.com/events", {
headers: { Accept: "text/event-stream" }
})
const first = res.body.read()
res.body.close()
throw { status: 200, json: { status: res.status, first: first } }
})()
```
**Fehlerfall (Error):** Ohne URL brichst du mit `missing url in http.fetchStream function` ab (`context_http.go:37`); `options` als String statt Objekt scheitert mit `options must be object in http.fetchStream function` (`context_http.go:52`), belegt durch die Test-Assertion auf diesen Text (`context_http_test.go:452`).
## smtp – Mails versenden
Quelle: `context_smtp.go`, registriert per `RegisterPackageFunctions("smtp", ...)` mit `sendMail`. Der Versand nutzt den Server-Mail-Host aus der Config oder einen per `host`, `username` und `password` übergebenen Host. Beachte: Die Error-Texte nennen wörtlich `mail.send`, obwohl das Paket in JS `smtp` heißt — Altlast-Namensrest im Server-Code, kein Alias.
### smtp.sendMail
**Signatur:** `smtp.sendMail(options: { to: string | Array, cc?: string | Array, bcc?: string | Array, from: string, fromName?: string, replyTo?: string, subject?: string, plain?: string, html?: string, attach?: string | Array, host?: string, username?: string, password?: string }): void` — aus `// args: options:map[string]interface{}` (`context_smtp.go:23`). Gibt bei Erfolg `null` zurück; Anhänge sind Pfade im Config-Verzeichnis.
```js title="smtp.sendMail – Beispiel"
;(function () {
context.smtp.sendMail({
to: "redaktion@example.com",
from: "noreply@example.com",
fromName: "Tibi CMS",
subject: "Neuer Beitrag",
plain: "Ein neuer Beitrag wartet auf Freigabe."
})
throw { status: 200, json: { sent: true } }
})()
```
**Fehlerfall (Error):** Ohne `options` scheitert der Aufruf mit `missing options in mail.send function` (`context_smtp.go:37`); ohne Empfänger meldet der Code `missing or invalid to in options of mail.send function` (`context_smtp.go:76`), ohne Absender `missing or invalid from in options of mail.send function` (`context_smtp.go:117`) — beides belegt in `context_smtp_test.go:77` („error - missing options") und `context_smtp_test.go:88` („error - missing from").
## channel – Realtime-Nachrichten
Quelle: `context_channel.go`, registriert per `RegisterPackageFunctions("channel", ...)` mit `subscribe`, `send`. `subscribe` blockiert per `receive()`, bis eine Nachricht eintrifft, der Client trennt (`null`) oder die Subscription geschlossen wird (Exception mit Code `channel_closed`); der Hook-Timeout wird dafür deaktiviert.
### channel.subscribe
**Signatur:** `channel.subscribe(name: string, options?: { bufferSize?: number, onFull?: string, messageTTL?: number, lastN?: number, maxAge?: number }): { receive(), close() }` — aus `// JS signature: channel.subscribe(name: string, options?: object): Subscription` (`context_channel.go:21`).
```js title="channel.subscribe – Beispiel"
;(function () {
const sub = context.channel.subscribe("news", { lastN: 10 })
const msg = sub.receive()
sub.close()
throw { status: 200, json: { message: msg } }
})()
```
**Fehlerfall (Error):** Außerhalb von Hooks (in Jobs) scheitert der Aufruf mit `channel.subscribe is only available in hooks, not in jobs` (`context_channel.go:29`); ohne Kanalnamen meldet der Code `channel.subscribe requires a channel name` (`context_channel.go:38`).
### channel.send
**Signatur:** `channel.send(name: string, data: any): void` — aus `// JS signature: channel.send(name: string, data: any): void` (`context_channel.go:109`). Publiziert an alle Subscriber des Kanals.
```js title="channel.send – Beispiel"
;(function () {
context.channel.send("news", { title: "Eilmeldung", active: true })
throw { status: 200, json: { sent: true } }
})()
```
**Fehlerfall (Error):** Ohne Kanalnamen und Daten scheitert der Aufruf mit `channel.send requires a channel name and data` (`context_channel.go:117`); ein leerer Kanalname liefert `channel.send: channel name must be a non-empty string` (`context_channel.go:121`).
## ratelimit – Backoff und Sliding Window
Quelle: `context_ratelimit.go`, registriert per `RegisterPackageFunctions("ratelimit", ...)` mit `backoffCheck`, `backoffRecord`, `backoffReset`, `windowCheck`, `windowReset`, `windowCount`. Alle sechs Funktionen brauchen denselben `key` (nicht-leerer String) und einen verfügbaren Limiter.
### ratelimit.backoffCheck
**Signatur:** `ratelimit.backoffCheck(key: string): { blocked: boolean, retryAfterMs: number }` — aus `// JS signature: ratelimit.backoffCheck(key: string): { blocked: boolean, retryAfterMs: number }` (`context_ratelimit.go:41`).
```js title="ratelimit.backoffCheck – Beispiel"
;(function () {
const state = context.ratelimit.backoffCheck("login:mailer@example.com")
if (state.blocked) {
throw { status: 429, json: { retryAfterMs: state.retryAfterMs } }
}
throw { status: 200, json: { blocked: false } }
})()
```
**Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`); ohne verfügbaren Limiter meldet der Code `ratelimit: limiter not available` (`context_ratelimit.go:23`).
### ratelimit.backoffRecord
**Signatur:** `ratelimit.backoffRecord(key: string): void` — aus `// JS signature: ratelimit.backoffRecord(key: string): void` (`context_ratelimit.go:59`). Zeichnet einen Fehlversuch auf und verschärft das exponentielle Backoff.
```js title="ratelimit.backoffRecord – Beispiel"
;(function () {
context.ratelimit.backoffRecord("login:mailer@example.com")
throw { status: 200, json: { recorded: true } }
})()
```
**Fehlerfall (Error):** Ein leerer Schlüssel scheitert mit `ratelimit: key must be a non-empty string` (`context_ratelimit.go:34`).
### ratelimit.backoffReset
**Signatur:** `ratelimit.backoffReset(key: string): void` — aus `// JS signature: ratelimit.backoffReset(key: string): void` (`context_ratelimit.go:74`). Setzt den Backoff-Zähler zurück, z. B. nach erfolgreichem Login.
```js title="ratelimit.backoffReset – Beispiel"
;(function () {
context.ratelimit.backoffReset("login:mailer@example.com")
throw { status: 200, json: { reset: true } }
})()
```
**Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`).
### ratelimit.windowCheck
**Signatur:** `ratelimit.windowCheck(key: string): { allowed: boolean, retryAfterMs: number }` — aus `// JS signature: ratelimit.windowCheck(key: string): { allowed: boolean, retryAfterMs: number }` (`context_ratelimit.go:89`). Zählt den Aufruf als Hit und prüft das Sliding Window in einem Schritt.
```js title="ratelimit.windowCheck – Beispiel"
;(function () {
const check = context.ratelimit.windowCheck("api:mailer@example.com")
if (!check.allowed) {
throw { status: 429, json: { retryAfterMs: check.retryAfterMs } }
}
throw { status: 200, json: { allowed: true } }
})()
```
**Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`); ohne verfügbaren Limiter meldet der Code `ratelimit: limiter not available` (`context_ratelimit.go:23`).
### ratelimit.windowReset
**Signatur:** `ratelimit.windowReset(key: string): void` — aus `// JS signature: ratelimit.windowReset(key: string): void` (`context_ratelimit.go:107`). Setzt das Sliding Window für den Schlüssel zurück.
```js title="ratelimit.windowReset – Beispiel"
;(function () {
context.ratelimit.windowReset("api:mailer@example.com")
throw { status: 200, json: { reset: true } }
})()
```
**Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`).
### ratelimit.windowCount
**Signatur:** `ratelimit.windowCount(key: string): number` — aus `// JS signature: ratelimit.windowCount(key: string): number` (`context_ratelimit.go:122`). Liefert die aktuelle Trefferzahl im Fenster, ohne zu zählen.
```js title="ratelimit.windowCount – Beispiel"
;(function () {
const n = context.ratelimit.windowCount("api:mailer@example.com")
throw { status: 200, json: { hits: n } }
})()
```
**Fehlerfall (Error):** Ohne Schlüssel scheitert der Aufruf mit `ratelimit: key argument required` (`context_ratelimit.go:30`).
Weiter: [Hook-Kontexte](/reference/hook-kontexte/) als Paket-Index, [Hooks schreiben](/projekt/hooks-schreiben/) für Lifecycle und Starter-Muster.
Verifiziert am 68b42785 2026-10-16 (tibi-server)
@@ -0,0 +1,209 @@
---
title: 'Hook-Referenz: Sicherheit und Identität'
description: 'jwt, user, bcrypt, crypto, cookie, exec und config im Hook-JS: Signaturen aus internal/hook, je ein Beispiel und je ein belegter Fehlerfall.'
---
In Hooks sprichst du diese sieben Pakete über `context` an, zum Beispiel zum Token-Erstellen, Passwort-Hashen oder Cookie-Setzen. Die Signaturen stammen aus den `// args:`-Kommentaren in `internal/hook/context_*.go` im tibi-server; wo so ein Kommentar fehlt, ist die Signatur aus dem Funktionsrumpf abgeleitet und entsprechend markiert. Jede Funktion zeigt genau einen Fehlerfall mit dem originalen Error-Text aus Code oder Tests (Hook-Fehler enden im Test mit Status `500`).
## jwt – Tokens erstellen und prüfen
Quelle: `context_jwt.go`, registriert per `RegisterPackageFunctions("jwt", ...)` mit `create` und `parse`. Ohne `secret`-Option nutzt der Server ein zufälliges Temporär-Secret (`jwtTempSecret`, `context_jwt.go:21`).
### jwt.create
**Signatur:** `jwt.create(claims: object, options?: { validityDuration?: number, secret?: string }): string` — aus `// args: claims:map[string]interface{}, options:map[string]interface{}` (`context_jwt.go:27`). `validityDuration` setzt `exp` auf jetzt plus Sekunden.
```js title="jwt.create – Beispiel"
;(function () {
const token = context.jwt.create({ sub: "user123", role: "admin" }, { validityDuration: 3600 })
throw { status: 200, json: { token: token } }
})()
```
**Fehlerfall (Error):** Ohne Claims brichst du mit `missing claims in jwt.create function` ab, Status `500` (`error - missing claims`, `context_jwt_test.go:109`); Claims als String statt Objekt: `claims must be object in jwt.create function` (`context_jwt.go:42`).
### jwt.parse
**Signatur:** `jwt.parse(token: string, options?: { secret?: string }): object` — aus `// args: token:string, options:map[string]interface{}` (`context_jwt.go:84`). Antwortet mit `{ valid, claims, header, method, error? }` und wirft bei ungültigen Tokens keinen Error: falsches Secret oder abgelaufenes Token liefern `valid: false` plus `error`-Text bei Status `200`.
```js title="jwt.parse – Beispiel"
;(function () {
const token = context.jwt.create({ sub: "user123" }, { secret: "s3cr3t" })
const res = context.jwt.parse(token, { secret: "s3cr3t" })
throw { status: 200, json: { valid: res.valid } }
})()
```
**Fehlerfall (Error):** Ohne Token: `missing token in jwt.parse function`, Status `500` (`error - missing token`, `context_jwt_test.go:120`); Token als Nicht-String: `token must be string in jwt.parse function` (`context_jwt.go:96`).
## user – Aktuellen Login lesen
Quelle: `context_user.go`, registriert per `RegisterPackageFunctions("user", ...)` mit `auth`. Liefert `{ id, username, role, permissions }` oder `null` ohne Login.
### user.auth
**Signatur (abgeleitet):** `user.auth(): object | null` — kein `args:`-Kommentar, abgeleitet aus dem Rumpf `FuncUserAuth` (`context_user.go:16`): nimmt keine Argumente und liest den API-Kontext.
```js title="user.auth – Beispiel"
;(function () {
const me = context.user.auth()
throw { status: 200, json: { login: me !== null, name: me && me.username } }
})()
```
**Fehlerfall (Error):** Außerhalb von Hooks (ohne API-Kontext) scheitert der Aufruf mit `user.auth is only valid in hooks` (`context_user.go:19`); ohne Login bekommst du `null` zurück, keinen Error (`auth not logged in`, `context_user_test.go:44`).
## bcrypt – Passwörter hashen und prüfen
Quelle: `context_bcrypt.go`, registriert per `RegisterPackageFunctions("bcrypt", ...)` mit `hash` und `check`.
### bcrypt.hash
**Signatur:** `bcrypt.hash(password: string, options?: { cost?: number }): string` — aus `// args: password:string, options:map[string]interface{}` (`context_bcrypt.go:18`); Standard-`cost` ist `14`.
```js title="bcrypt.hash – Beispiel"
;(function () {
const hash = context.bcrypt.hash("s3cr3t-pw")
throw { status: 200, json: { hash: hash } }
})()
```
**Fehlerfall (Error):** Ohne Passwort: `missing password in bcrypt.hash function`, Status `500` (`error - missing password`, `context_bcrypt_test.go:91`); falscher `cost`-Typ: `invalid type for costs in bcrypt.hash function` — so wörtlich im Code, mit Plural-s (`context_bcrypt.go:42`).
### bcrypt.check
**Signatur:** `bcrypt.check(password: string, hash: string): boolean` — aus `// args: password:string, hash:string` (`context_bcrypt.go:56`). Ein falsches Passwort liefert `false`, keinen Error.
```js title="bcrypt.check – Beispiel"
;(function () {
const hash = context.bcrypt.hash("s3cr3t-pw")
const ok = context.bcrypt.check("s3cr3t-pw", hash)
throw { status: 200, json: { match: ok } }
})()
```
**Fehlerfall (Error):** Weniger als zwei Argumente: `missing password and/or hash in bcrypt.check function`, Status `500` (`error - missing args in check`, `context_bcrypt_test.go:102`).
## crypto – Hashes bilden
Quelle: `context_crypto.go`, registriert per `RegisterPackageFunctions("crypto", ...)` mit `sha256` und `md5`. Das sind Einweg-Hashes für Prüfsummen, kein Passwort-Hashing — dafür nimmst du das bcrypt-Paket.
### crypto.sha256
**Signatur:** `crypto.sha256(text: string): string` — aus `// args: string:string` (`context_crypto.go:19`). Liefert Hex, belegt mit `"hello"` → `2cf24dba…9824` in `context_crypto_test.go:22`.
```js title="crypto.sha256 – Beispiel"
;(function () {
const sum = context.crypto.sha256("hello")
throw { status: 200, json: { sha256: sum } }
})()
```
**Fehlerfall (Error):** Ohne Argument: `missing string in crypto.sha256 function`, Status `500` (`error - sha256 missing argument`, `context_crypto_test.go:77`).
### crypto.md5
**Signatur:** `crypto.md5(text: string): string` — aus `// args: string:string` (`context_crypto.go:33`). Liefert Hex, belegt mit `"hello"` → `5d41402a…c592` in `context_crypto_test.go:44`.
```js title="crypto.md5 – Beispiel"
;(function () {
const sum = context.crypto.md5("hello")
throw { status: 200, json: { md5: sum } }
})()
```
**Fehlerfall (Error):** Ohne Argument: `missing string in crypto.md5 function`, Status `500` (`error - md5 missing argument`, `context_crypto_test.go:88`).
## cookie – Cookies lesen und setzen
Quelle: `context_cookie.go`, registriert per `RegisterPackageFunctions("cookie", ...)` mit `get` und `set`. Beide brauchen einen Hook-HTTP-Kontext.
### cookie.get
**Signatur:** `cookie.get(name: string): string` — aus `// args: name:string` (`context_cookie.go:17`). Unbekannte Cookies liefern `""`, keinen Error.
```js title="cookie.get – Beispiel"
;(function () {
const session = context.cookie.get("session")
throw { status: 200, json: { hasSession: session !== "" } }
})()
```
**Fehlerfall (Error):** Ohne Namen: `missing cookie name in cookie.get`, Status `500` (`error - missing cookie name`, `context_cookie_test.go:55`); außerhalb von Hooks: `cookie functions only valid in hooks` (`context_cookie.go:20`).
### cookie.set
**Signatur:** `cookie.set(name: string, data: string, options?: object): void` — aus `// args: name:string options:map[string]interface{}` (`context_cookie.go:40`). `options` kennt `maxAge`, `path`, `domain`, `httpOnly`, `secure` und `sameSite` (`strict`, `lax` oder `none`).
```js title="cookie.set – Beispiel"
;(function () {
context.cookie.set("session", "abc123", { maxAge: 3600, path: "/", httpOnly: true, sameSite: "lax" })
throw { status: 200, json: { set: true } }
})()
```
**Fehlerfall (Error):** Weniger als zwei Argumente: `missing cookie name and/or data in cookie.set`, Status `500` (`error - missing cookie data`, `context_cookie_test.go:66`); unbekanntes `sameSite`: `invalid value for sameSite in cookie.set function` (`context_cookie.go:136`).
## exec – Kommandos ausführen
Quelle: `context_exec.go`, registriert per `RegisterPackageFunctions("exec", ...)` mit `command`. Läuft mit Timeout (Standard `10000` ms) im Projektverzeichnis und unter der Config-UID.
### exec.command
**Signatur:** `exec.command(command: string, options?: { args?: Array, stdin?: string, dir?: string, env?: Array, timeout?: number, returnObject?: boolean, combineOutput?: boolean }): string | object` — aus `// args: command:string, options:map[string]interface{}` (`context_exec.go:22`). Ohne `returnObject` kommt stdout als String; mit `returnObject: true` antwortet `{ stdout, stderr, exitCode }` (bzw. `{ output, exitCode }` mit `combineOutput`).
```js title="exec.command – Beispiel"
;(function () {
const out = context.exec.command("echo", { args: ["Hallo Hook"] })
throw { status: 200, json: { result: out } }
})()
```
**Fehlerfall (Error):** Ohne Kommando: `missing command in exec.command function` (`context_exec.go:38`); Exit-Code ungleich 0 ohne `returnObject`: `exec.command failed with exit code 1`, Status `500` (`test exec command error case`, `context_exec_test.go:65`); bei Timeout: `exec.command timed out after ... ms` (`context_exec.go:183`).
## config – Server- und Projekt-Config lesen
Quelle: `context_config.go`, registriert per `RegisterPackageFunctions("config", ...)` mit `namespace`, `project` und `server`. Alle drei ohne `args:`-Kommentar — Signaturen aus den Rümpfen abgeleitet (`context_config.go:19`, `:23`, `:27`).
### config.namespace
**Signatur (abgeleitet):** `config.namespace(): string` — liefert den Namespace, nimmt keine Argumente.
```js title="config.namespace – Beispiel"
;(function () {
const ns = context.config.namespace()
throw { status: 200, json: { namespace: ns } }
})()
```
**Fehlerfall:** Kein Fehlerfall — `namespace` liest nur ein Feld und kennt keinen Error-Pfad (dokumentierte Abwesenheit, kein Versäumnis).
### config.project
**Signatur (abgeleitet):** `config.project(): object` — liefert die Projekt-Config als Map, nimmt keine Argumente.
```js title="config.project – Beispiel"
;(function () {
const project = context.config.project()
throw { status: 200, json: { name: project.name } }
})()
```
**Fehlerfall:** Kein Fehlerfall — `project` mappt nur die Config und kennt keinen Error-Pfad (dokumentierte Abwesenheit, kein Versäumnis).
### config.server
**Signatur (abgeleitet):** `config.server(): object` — liefert die Server-Config als Map, nimmt keine Argumente.
```js title="config.server – Beispiel"
;(function () {
const server = context.config.server()
throw { status: 200, json: { port: server.api && server.api.port } }
})()
```
**Fehlerfall (Error):** Kein fixer Error-String im Code — nur interne JSON-Marshal-Fehler werden durchgereicht (`context_config.go:30`); im Test läuft der Aufruf fehlerfrei mit Status `200` (`context_config_test.go:19`).
Weiter: [Hook-Kontexte](/reference/hook-kontexte/) als Paket-Index, [Hooks schreiben](/projekt/hooks-schreiben/) für Lifecycle und Starter-Muster.
Verifiziert am 68b42785 2026-10-16 (tibi-server)
+5
View File
@@ -0,0 +1,5 @@
---
title: 'OpenAPI (generiert)'
---
> Stand: Platzhalter · Quelle: `TIBI_API_URL/openapi.json` — nicht von Hand editieren. Erzeugen mit `npm run sync:openapi`.
+81
View File
@@ -0,0 +1,81 @@
---
title: 'Schemas als Quelle'
description: 'Welche tibi-types-Schemas wofür gelten und wie du sie liest.'
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Deine `api/*.yml`-Dateien sind durch JSON-Schemas geprüft. Die Schemas liegen in `tibi-types/schemas/config/`. Jede YML-Datei verweist oben per `$schema`-Zeile auf ihr Schema.
## Schema-Liste
Das Verzeichnis enthält 16 Dateien (nicht 15, wie ältere Docs behaupten):
| Schema | Gilt für | Pflichtfelder |
| --- | --- | --- |
| `project.schema.json` | `api/config.yml` | `namespace` |
| `collection.schema.json` | `api/collections/*.yml` | `name`, `permissions` |
| `field.schema.json` | einzelnes Feld | Typ-abhängig |
| `field-list.schema.json` | Feld-Listen | — |
| `field-meta.schema.json` | `meta:` an Feldern | keine (alle optional) |
| `collection-meta.schema.json` | `meta:` an Collections | keine (alle optional) |
| `permissions.schema.json` | `permissions:`-Block | `public`, `user` |
| `hooks.schema.json` | `hooks:`-Block | keine (`get`, `post`, `put`, `delete`, `audit` optional) |
| `job.schema.json` | Jobs in `config.yml` | `type`, `file` |
| `action.schema.json` | Actions in `config.yml` | `name` |
| `asset.schema.json` | Assets in `config.yml` | `name`, `path` |
| `image-filter.schema.json` | `imageFilter:` an Collections | — |
| `projections.schema.json` | `projections:`-Block | — |
| `snapshot.schema.json` | Snapshots in `config.yml` | `name` |
| `openapi.schema.json` | OpenAPI-Metadaten je Collection | keine |
| `defs.schema.json` | nur `$ref`-Bausteine, kein eigenes Ziel | — |
## Lesart
<Tabs>
<TabItem label="Kopfzeile">
```yaml title="api/collections/content.yml"
# yaml-language-server: $schema=../../tibi-types/schemas/config/collection.schema.json
name: content
permissions:
public:
methods:
get: true
```
Der Editor prüft live gegen das Schema. Tippfehler in Schlüsseln meldet er sofort.
</TabItem>
<TabItem label="Pflicht gegen Code prüfen">
Das Schema prüft die **Form**. Ob der Server die Werte so versteht, steht im Code:
- `collection.schema.json` verlangt `permissions` — was `public` und `user` dürfen, entscheidet `internal/models/permission.go` im tibi-server.
- `field.schema.json` kennt Typen wie `string`, `number`, `date` — was davon wirklich validiert wird, steht in `internal/controller/validator.go`.
- `project.schema.json` kennt `admin.tokens` — was diese Tokens dürfen, steht in `internal/config/admin_token.go`.
</TabItem>
</Tabs>
<Aside type="caution" title="Schema deckt nicht alles ab">
Ein grünes Schema heißt nur: die Datei ist wohlgeformt. Falsche Token-Namen oder fehlende Berechtigungen meldet erst der Server zur Laufzeit. Lies dazu die Seiten zu Token-Köpfen und Fehlerbildern.
</Aside>
## Text-Diagramm: Wo was geprüft wird
```text title="Prüfebenen im Überblick"
api/config.yml ──$schema──▶ project.schema.json (Form)
│
▼
tibi-server beim Reload (Bedeutung)
├── permission.go → wer darf was?
├── validator.go → ist der Datensatz gültig?
└── admin_token.go → welches Token gilt wofür?
```
## Nächste Schritte
- Lege ein neues Projekt an: Einstieg Neues Projekt.
- Schlage einen Hook-Kontext nach: Hook-Kontexte.
- Verstehe Fehlermeldungen: Fehlerbilder.
Verifiziert am 4c55f82 2026-10-12 (tibi-types)
@@ -0,0 +1,82 @@
---
title: 'Tastatur und Schnellsuche'
description: 'Alle Tastenkürzel, die Befehlspalette und die Breadcrumb-Navigation im tibi-admin.'
---
import { Aside } from '@astrojs/starlight/components';
Du musst die Maus nicht anfassen: Fast alles im **tibi-admin** erreichst du per Taste oder über die Befehlspalette.
`?` zeigt die Kürzel-Übersicht direkt im Admin. Alle Bezeichnungen unten stehen wörtlich so in der Oberfläche (`de.json`).
Suchbegriffe: Shortcuts, Hotkeys, Command Palette, Schnellsuche, Brotkrumen.
## Regeln zuerst
- Einzel-Tasten (`s`, `n`, `/` …) wirken **nur außerhalb von Eingabefeldern** — sobald du in Input, Textarea oder Editor stehst, tippen sie Text.
- `Ctrl`-Kombinationen (`Ctrl+S`, `Ctrl+F`, `Ctrl+L`) wirken **auch in Eingabefeldern**.
- `Esc` schließt Hilfe-Overlay, Befehlspalette und offene Dialoge.
- Akkorde (`g → p`, `p → 1`) tippst du nacheinander, nicht gleichzeitig. Nach dem ersten Buchstaben hast du etwa eine halbe Sekunde Zeit; ein Hinweis-Chip unten zeigt den wartenden Buchstaben.
## Kürzel-Tabelle
| Taste | Wo | Was |
|---|---|---|
| `s` | Liste | „Suche fokussieren“ — springt ins Suchfeld der Liste (siehe [Ansichten und Listen](/admin/ansichten-listen/)) |
| `f` | Liste | „Filter öffnen“ — öffnet den Filter-Dialog |
| `x` | Liste | „Filter zurücksetzen“ — löscht alle Filter |
| `o` | Liste | „Sortierung öffnen“ — öffnet die Sortier-Auswahl |
| `v` | Liste | „Ansicht umschalten“ — wechselt zwischen „Tabellenansicht“ und „Kartenansicht“ |
| `[` / `]` | Liste | „Vorherige Seite“ / „Nächste Seite“ — blättert seitenweise |
| `p → 1` … `p → 9` | Liste | „Zu Seite springen“ — springt direkt auf Seite 1 bis 9 |
| `p → 0` | Liste | „Seitengröße wählen“ — öffnet die Palette mit der Suche „pro seite“ (10 / 25 / 50 pro Seite) |
| `↑` `↓` | Liste | „Einträge navigieren“ — bewegt den Fokus durch die Zeilen (Listen-eigen) |
| `Enter` | Liste | „Eintrag öffnen“ — öffnet den fokussierten Eintrag (Listen-eigen) |
| `Leertaste` | Liste | „Eintrag auswählen/abwählen“ — setzt den Haken beim fokussierten Eintrag (Listen-eigen) |
| `a` | Liste | „Alle auswählen / aufheben“ — markiert alle Zeilen oder hebt die Auswahl auf |
| `n` | Liste | „Neues Element“ — legt einen Eintrag an (nur mit Anlege-Recht) |
| `e` | Liste | „Massenbearbeitung“ — öffnet die Bulk-Aktion (nur mit Änderungs-Recht) |
| `d` | Liste | „Eintrag/Einträge löschen“ — löscht Auswahl oder fokussierten Eintrag (nur mit Lösch-Recht) |
| `Shift+N` | Editor | „Eintrag duplizieren“ — legt eine Kopie des offenen Eintrags an |
| `Ctrl+S` | Editor | „Eintrag speichern" — speichert (Editor-eigen, siehe [Eintrags-Editor](/admin/eintrags-editor/)) |
| `Ctrl+Z` / `Ctrl+Shift+Z` / `Ctrl+Y` | Editor | Rückgängig / Wiederholen der letzten Änderung (nicht in nativen Eingabefeldern) |
| `Ctrl+F` | Editor / Liste | „Feld suchen / Suche fokussieren“ — im Editor öffnet es die Feldsuche, in der Liste fokussiert es die Suche |
| `Ctrl+Shift+F` | Liste | Wechselt den Suchmodus (wirkt nur, wenn die Sammlung mehrere Suchmodi hat) |
| `Ctrl+L` | Editor | „Inhaltssprache wechseln“ — rotiert die Bearbeitungs-Sprache |
| `j`, dann `1` … `9` | Seite | „Zur Sektion springen“ — springt zur n-ten registrierten Sektion |
| `j`, dann `0` | Seite | „Nach oben scrollen“ — springt zum Seitenanfang (wie `t`) |
| `j` allein | Seite | Öffnet die Befehlspalette mit der Suche „sektion“ zur Auswahl |
| `t` | Überall | „Nach oben“ — scrollt zum Seitenanfang |
| `g → p` | Überall | „Zu Projekte“ — zurück zur Projektliste |
| `g → u` | Überall | „Zur Benutzerverwaltung“ — öffnet `/admin/users` (nur mit Recht sichtbar) |
| `/` | Überall | „Befehlspalette“ — öffnet oder schließt die Palette |
| `?` | Überall | „Shortcuts anzeigen“ — öffnet diese Übersicht als Overlay |
| `c` | Überall | „Nova ein-/ausblenden“ — öffnet oder schließt den KI-Chat (nur wenn Nova eingeschaltet ist) |
| `Shift+C` | Überall | „Neuen Chat anfangen“ — startet einen frischen Nova-Chat (nur wenn Nova eingeschaltet ist) |
| `m` | Überall | „Spracheingabe im Chat“ — Diktat im offenen Chat (nur wenn Nova an ist und der Browser es kann) |
| `Shift+M` | Überall | „Spracheingabe in neuem Chat“ — Diktat mit neuem Chat (Bedingung wie `m`) |
<Aside type="caution" title="Hilfe-Overlay zeigt „u“ allein — Taste tut nichts">
Die Overlay-Zeile mit `u` hat keinen passenden Text in der Sprachdatei (Schlüssel `toggleUsers` fehlt) und keinen eigenen Handler.
Drücke stattdessen den Akkord `g → u` für „Zur Benutzerverwaltung“.
Ebenfalls nicht im Overlay, aber belegt: `Shift+N` („Eintrag duplizieren“).
</Aside>
## Befehlspalette (`/`)
Die Palette findet **Navigationsziele und Befehle**, keine Inhalte: Projekte, Sammlungen und Aktionen zum Anspringen, dazu Ansichts-, Sortier-, Seiten-, Darstellungs-, Sprach-, Nova-, System- und Debug-Befehle sowie „Springen zu“-Sektionen der aktuellen Seite.
Was sie **nicht** findet: Feldwerte oder Datensätze — dafür nimmst du die Listensuche (`s`) bzw. im Editor die Feldsuche (`Ctrl+F`).
So benutzt du sie: `/` drücken, lostippen (unscharfe Suche über Name, Stichwort und Kategorie, mehrere Wörter mit Leerzeichen trennen), mit `↑` `↓` wählen, `Enter` startet den Befehl, `Esc` schließt.
Die Trefferliste ist kontextabhängig: In einer Liste kommen Ansichts- und Sortierbefehle dazu (Sortieren pro Feld nur für Text-, Zahl-, Datums- und Ja/Nein-Felder, Richtext ausgenommen), im Editor der Befehl „Eintrag als JSON bearbeiten“.
Manche Befehle erscheinen nur mit passendem Recht (z. B. „Massenbearbeitung“, „Eintrag/Einträge löschen“, „Neues Element“) oder nur wenn Nova läuft.
## Breadcrumb-Navigation
Über der Ansicht zeigt der Pfad deinen Standort: Haus-Symbol (Projektliste), Projekt, Sammlung, Eintrag.
Jedes Segment außer dem aktuellen ist klickbar und navigiert ohne Neuladen innerhalb der App.
Das Haus-Symbol führt zurück zur Projektliste, das Sammlungs-Segment zurück in die Liste — dabei bleiben Suche, Filter und Seitennummer erhalten (gemerkte Rücksprung-Adresse).
Der Browser-Zurück-Button gehört dazu: Nach dem Eintritt ins Projekt stellt „Zurück“ die vorige Ansicht wieder her, „Vorwärts“ die Sammlung.
Hat der Eintrag ungespeicherte Änderungen, blockiert erst ein Schutz-Dialog den Weggang — erst deine Bestätigung („Verwerfen“) setzt die Navigation fort.
In verschachtelten Seiten-Panels (Drawern) zeigt eine zweite, kompakte Pfadzeile den Ebenen-Pfad („Navigationspfad“); auf schmalen Bildschirmen klappt sie mittlere Ebenen zu „…“ zusammen, Antippen zeigt den vollen Pfad („Vollständigen Pfad anzeigen“).
Verifiziert am 2b091d89 2026-10-13 (tibi-admin-nova: shortcuts.ts, KeyboardShortcuts-, CommandPalette-, Breadcrumb-, DrawerBreadcrumb-Komponenten, de.json, Test-Plan 02).
@@ -0,0 +1,67 @@
---
title: 'Token-Köpfe'
description: 'Welcher Kopf wofür: Token, X-Auth-Token, X-Admin-Token, Bearer.'
---
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
Jeder Request braucht den passenden Kopf im passenden Header. Falscher Kopf heißt fast immer `permission denied` oder `project not found`.
## Tabelle: Vier Köpfe
| Header | Wert | Wofür | Woher |
| --- | --- | --- | --- |
| `Token` | `${ADMIN_TOKEN}` aus `api/config.yml.env` | Collection-Schreibzugriffe (z. B. Content anlegen) | Name `token` steht im Permission-Schlüssel `"token:${ADMIN_TOKEN}"` in `api/collections/*.yml` |
| `X-Auth-Token` | JWT aus Login | Projekt anlegen, Reload, User-Requests | `POST /api/v1/login` mit `admin` / `admin` (lokaler Starter-Stack) |
| `X-Admin-Token` | Server-Admin-Token | System-APIs wie Projekt-CRUD, Reload | Server-Config des laufenden tibi-servers |
| `Authorization: Bearer …` | Gleicher Wert wie `X-Admin-Token` | Alias für Server-Admin-Token; Deploy-Skripte nutzen ihn am Reload-Endpunkt | Server-Config bzw. `api/config.yml.env` |
<Tabs>
<TabItem label="Collection schreiben">
```sh title="Dokument anlegen"
curl -s -X POST "$URL/api/content" \
-H "Token: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Hallo"}'
```
Der Header-Name `Token` passt zum Schlüssel `"token:${ADMIN_TOKEN}"` in der Collection-YML (Groß-/Kleinschreibung egal).
</TabItem>
<TabItem label="Projekt registrieren">
```sh title="JWT holen und Projekt anlegen"
jwt=$(curl -s -X POST "$ADMIN_URL/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin"}' | jq -r '.token')
curl -s -X POST "$SERVER_URL/api/v1/project" \
-H "X-Auth-Token: $jwt" \
-H "Content-Type: application/json" \
-d '{"name":"meinprojekt","configFile":"/data/api/config.yml","enabled":true}'
```
</TabItem>
</Tabs>
<Aside type="note" title="Zwei Login-Pfade, ein Login">
Direkt am Server heißt die Route `/api/v1/login` (`setup.go`: Gruppe `/api/v1` + `/login`). Im Starter-Stack proxyt der Admin-Host `/api/...` auf den Server — deshalb steht im Beispiel `$ADMIN_URL/api/login`. Beide meinen denselben Login, nur auf verschiedenen Wegen.</Aside>
## Verwechslungs-Fallen
- **Projekt-Token ist kein Admin-Token.** `Token: $ADMIN_TOKEN` schreibt Collections, registriert aber keine Projekte. `GET /api/v1/project` damit antwortet `permission denied`.
- **`ADMIN_TOKEN` kann keine Projekte anlegen.** Nur Sammlungs-Zugriffe plus Reload/Snapshot laut `admin.tokens` in `api/config.yml`. Für Projekt-CRUD brauchst du JWT oder Server-Admin-Token.
- **Reservierte Namen.** `x-auth-token`, `x-admin-token` und `authorization` dürfen nie als Collection-Token-Name in `permissions` stehen — der Server ignoriert sie dort fail-closed (`internal/models/permission.go`).
- **Query statt Header geht auch.** Collection-Token akzeptiert der Server zusätzlich als Query-Parameter mit gleichem Namen. Nutze das nur zum Testen, nie im Produktiv-Frontend.
## Fehlermeldungen
| Antwort | Ursache | Fix |
| --- | --- | --- |
| `permission denied: admin token lacks permission project.read` | Collection-Token an System-API gesendet | JWT via `X-Auth-Token` oder `X-Admin-Token` nehmen |
| `permission denied` (403) | Kopf fehlt oder Wert falsch | Header-Namen und `.env`-Wert prüfen |
| `project not found` (404) | Projekt im laufenden Server nicht registriert | Per JWT registrieren, dann Reload |
<Aside type="caution" title="Nicht mischen">
Ein funktionierender Collection-Token beweist keinen Projekt-Admin-Zugriff. Prüfe jeden Kopf an seinem eigenen Endpunkt.
</Aside>
Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter) und 68b42785 2026-10-15 (tibi-server)
+89
View File
@@ -0,0 +1,89 @@
---
title: 'Betrieb und Beobachtung'
description: 'Logs, Metriken, Suche neu aufbauen, Jobs und Audit aus Betreiber-Sicht.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du betreibst den **tibi-server** als Container. Diese Seite zeigt Startprofile, Gesundheit, Suche, Jobs und typische Störungen. Details zur Datei stehen unter [Server-Konfiguration](/server/konfiguration/).
## Startprofile
Der Starter (`docker-compose-local.yml`) kennt zwei Profile:
<Badge text="tibi" variant="tip" /> <Badge text="tibi-dev" variant="note" />
| Profil | Service | Wofür |
|---|---|---|
| `tibi` | `tibiserver` (Image `tibi-server:nova`) | Normalbetrieb: fertiges Image, Projekt unter `./` als `/data` gemountet |
| `tibi-dev` | `tibiserver-dev` (Build aus `tibi-server/Dockerfile.air`) | Entwicklung am Server: Live-Reload, Server-Code gemountet |
Dazu laufen `mongo` (MongoDB 7) und `maildev` (Mails auf `maildev:25`, Web-UI zum Lesen).
```bash title="Starten und Logs ansehen"
docker compose --profile tibi up -d
docker compose logs -f tibiserver
```
Beim Start loggt der Server die wirksame Konfiguration (`using following server config:`). Prüfe dort zuerst `DB.Dial`, `DB.Prefix` und `API.Port`.
## Gesundheit und Metriken
| Signal | Adresse | Was du siehst |
|---|---|---|
| Version | `GET /api/v1/version` | `{"version":"…"}` — der Server lebt und die Config lädt |
| Prometheus | `GET /metrics` | u. a. `tibi_request_duration_seconds` je Methode, Status, Namespace, Collection |
| Sentry | nur bei gesetzter `sentry.dsn` | Fehler + Traces mit Client-IP und Transaktionsname |
<Steps>
1. `curl http://localhost:8080/api/v1/version` — kommt JSON, läuft der Router.
2. `curl http://localhost:8080/metrics` — kommen `tibi_*`-Zeilen, läuft die Instrumentierung.
3. Antwortzeiten steigen? In `/metrics` nach `status`- und `col`-Labels filtern.
</Steps>
<Aside type="note" title="Audit">
`audit.enabled: true` schreibt System-Änderungen (User, Projekt, Login, Shutdown) mit TTL weg. Vorgabe sind 30 Tage (`720h` über `audit.defaultTTL`). Lies sie über `GET /api/v1/audit` (`audit.read`-Recht nötig).
</Aside>
## Suche und Reload
Die Volltextsuche läuft pro Projekt in einem Worker. Nach Config-Änderungen musst du neu laden:
| Endpunkt | Wirkung |
|---|---|
| `POST /api/v1/_/:project/_/admin/reload` | Lädt Projekt-Config neu (Recht `project.reload`) |
| `POST /api/v1/_/:project/_/admin/regenerate-search` | Baut Suchindex neu auf (`project.regenerate-search`) |
| `GET /api/v1/_/:project/_/admin/search-worker/status` | Status des Such-Workers |
| `GET /api/v1/_/:project/_/admin/embedding/status` | Status der Embedding-Provider |
```bash title="Nach Config-Änderung: Reload plus Suchindex neu aufbauen"
curl -X POST http://localhost:8080/api/v1/_/meinprojekt/_/admin/reload \
-H "X-Admin-Token: $TOKEN"
curl -X POST http://localhost:8080/api/v1/_/meinprojekt/_/admin/regenerate-search \
-H "X-Admin-Token: $TOKEN"
```
Backups und Snapshots meldet der Server per Fortschritts-Stream (SSE, `text/event-stream`). Bricht ein Download ab, hole zuerst die Analyse (`backup/analyze-download`), dann die Datei neu.
## Jobs
Jobs sind geplante Skripte je Projekt (Cron in der Projekt-`config.yml`, gleiche goja-Laufzeit wie Hooks):
- Sie laufen im `JOB_CONTEXT` — ohne HTTP-Request, mit fast allen Hook-Paketen.
- Sie laufen unter der Projekt-UID (suexec-Isolation, siehe [Sicherheit](/server/sicherheit/)).
- Schlägt ein Job fehl, steht die Ursache im Server-Log; Quotas und Nutzung landen in der LLM-Nutzungsablage.
## Störungen
| Bild | Ursache (häufig) | Maßnahme |
|---|---|---|
| Start-Panic `invalid number in env API_PORT` | Port fehlt oder `<= 0` | `API_PORT` setzen, siehe [Server-Konfiguration](/server/konfiguration/) |
| `version` antwortet nicht | Container down, falscher Port | `docker compose ps`, Logs prüfen |
| Login wird immer langsamer | Ratelimit-Backoff nach Fehlversuchen | Warten (Retry-After), Zugangsdaten prüfen |
| Suche findet nichts Neues | Worker hängt oder Index alt | Worker-Status abfragen, `regenerate-search` |
| 413 bei Upload | Body-Limit erreicht (Server → Projekt → Collection) | `API_UPLOAD_MAX_BODY_SIZE` bzw. Collection-Limit erhöhen |
| Leere Seite trotz Daten | CORS blockt den Browser | Origins in der Collection-Config prüfen |
Begriffe wie Collection, Hook oder Namespace klärt [Begriffe](/einstieg/begriffe/). Zum Einrichten siehe [Schnellstart](/einstieg/schnellstart/).
Verifiziert am `68b42785` 2026-10-15 (tibi-server); Compose-Profile am `e3f92ebf` 2026-10-14 (tibi-svelte-starter).
+140
View File
@@ -0,0 +1,140 @@
---
title: 'Server-Konfiguration'
description: 'config.yml und Env-Overrides des tibi-servers selbst (Betreiber-Sicht).'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du konfigurierst den **tibi-server** über eine `config.yml` plus Umgebungsvariablen. Env schlägt Datei. Die Wahrheit steht in `internal/config/server.go` (Struct-Tags).
```text title="Config-Vorrang"
config.yml (Datei) < Umgebungsvariable (gewinnt immer)
\ /
v v
Server.Server (Speicher)
```
## Datei und Env-Regeln
- Fehlt die Datei, startet der Server mit Notwerten: Port `8080`, Prefix `tibi`, Stacktraces an.
- `API_PORT` oder `PORT` sind Pflicht: Port `<= 0` beendet den Start per Panic.
- Namen mit Bindestrich werden in Env zu Unterstrich + Großschrift: `bge-m3` → `EMBEDDING_BGE-M3_*`.
- Listen in Env sind Komma-getrennt: `LLM_PROVIDERS=openai,lokal`.
## Die Bereiche im Überblick
| Bereich (`config.yml`) | Wichtigste Schlüssel | Env-Override |
|---|---|---|
| `db` | `dial`, `prefix`, `logging` | `DB_DIAL` (Fallback `MGO_DIAL`), `DB_PREFIX`, `DB_LOGGING` |
| `api` | `port`, `jwtSecret`, `upload.maxBodySize`, Token-Lifetimes | `API_PORT`/`PORT`, `API_JWTSECRET`, `API_UPLOAD_MAX_BODY_SIZE`, `API_ACCESS_TOKEN_LIFETIME`, `API_REFRESH_TOKEN_LIFETIME`, `API_SECURE_COOKIES` |
| `mail` | `host`, `username`, `password` | `MAIL_HOST` |
| `security` | `allowAbsolutePaths`, `allowUpperPaths` | `SECURITY_ALLOWABSOLUTEPATHS`, `SECURITY_ALLOWUPPERPATHS` |
| `response.error` | `stack`, `message` | `RESPONSE_ERROR_STACK`, `RESPONSE_ERROR_MESSAGE` |
| `audit` | `enabled`, `defaultTTL`, `defaultLimit`, `maxLimit` | `AUDIT_ENABLED`, `AUDIT_DEFAULT_TTL`, `AUDIT_DEFAULT_LIMIT`, `AUDIT_MAX_LIMIT` |
| `ratelimit` | `enabled`, `loginInitialDelay`, `loginMaxDelay`, `loginResetAfter` | `RATELIMIT_*` (gleiche Namen) |
| `hook` | `cache.*`, `ratelimit.*` | `HOOK_CACHE_*`, `HOOK_RATELIMIT_*` |
| `mcp` | `enabled`, `route`, `maxResultItems`, `allowAdminActions` | `MCP_ENABLED`, `MCP_ROUTE`, `MCP_MAX_RESULT_ITEMS`, `MCP_ALLOW_ADMIN_ACTIONS` |
| `sentry` | `dsn`, `environment`, `serverName`, `enableTracing`, `debug` | keine — nur Datei |
| `cors` | `allowOrigins`, `allowCredentials`, … | keine — wirksam ist die Projekt-/Collection-Ebene (siehe [Sicherheit](/server/sicherheit/)) |
| `llm` / `embedding` | Provider-Listen | dynamisch, siehe unten |
<Aside type="caution" title="README lügt an zwei Stellen">
Das README-Beispiel ist unvollständig und nennt <del>Go 1.23</del> <ins>go 1.25.0</ins> (Toolchain `go1.25.7`) laut `go.mod`. Verlasse dich nur auf die Struct-Tags in `server.go`.
</Aside>
## Datenbank, API, Mail
<Tabs>
<TabItem label="Minimal-Beispiel">
```yaml title="config.yml"
db:
dial: mongodb://mongo
prefix: meinprojekt
api:
port: 8080
jwtSecret: mindestens-32-zufallszeichen
mail:
host: maildev:25
```
</TabItem>
<TabItem label="Lokaler Dial (Starter)">
```yaml title="docker-compose-local.yml (Service tibiserver)"
environment:
DB_DIAL: mongodb://mongo
DB_PREFIX: ${TIBI_PREFIX}
MAIL_HOST: maildev:25
SECURITY_ALLOWABSOLUTEPATHS: "true"
SECURITY_ALLOWUPPERPATHS: "true"
RESPONSE_ERROR_STACK: "true"
```
Der Starter nutzt `mongodb://mongo` (Compose-Netz). Lokal ohne Compose heißt der Dial `mongodb://localhost:27017`. Der Prefix trennt Projekte in einer MongoDB.
</TabItem>
</Tabs>
Defaults, die du kennen musst:
- `secureCookies`: Standard `true`. Nur für reines HTTP-Lokalsetup auf `false` setzen.
- `accessTokenLifetime`: Standard `60m`. `refreshTokenLifetime`: Standard `10h`. Format ist Go-Duration (`30m`, `24h`).
- Ungültige Duration fällt still auf den Default zurück (Log-Warnung).
## LLM und Embedding per Env
Provider musst du nicht in die Datei schreiben. Diese Muster gelten:
```bash title="LLM-Provider anmelden"
LLM_PROVIDERS=openai,lokal-llm
LLM_OPENAI_APIKEY=sk-...
LLM_OPENAI_TYPE=openai # Vorgabe, wenn leer
LLM_OPENAI_BASEURL=https://api.openai.com/v1 # Vorgabe für Typ openai
LLM_OPENAI_DEFAULTMODEL=gpt-4o
LLM_OPENAI_MODELS=gpt-4o,gpt-4o-mini
```
```bash title="Embedding-Provider anmelden (Starter-Beispiel)"
EMBEDDING_PROVIDERS=bge-m3
EMBEDDING_BGE-M3_TYPE=native
EMBEDDING_BGE-M3_MODELPATH=/models/bge-m3
EMBEDDING_BGE-M3_DIMENSIONS=1024
EMBEDDING_BGE-M3_DOCUMENTPREFIX="search_document: "
EMBEDDING_BGE-M3_QUERYPREFIX="search_query: "
```
Gruppen gehen über `EMBEDDING_GROUPS=main` plus `EMBEDDING_GROUP_MAIN_MEMBERS=...`, `..._POLICY=...`, `..._TIMEOUT_TOTAL=...`.
## Admin-Tokens
Server-Admin-Tokens stehen unter `api.adminTokens`. Jedes Token hat `token`, `label` und `permissions`:
```yaml title="config.yml"
api:
adminTokens:
- token: ersetze-mich-durch-zufall
label: backup-job
permissions: ["project.backup", "project.reload"]
```
- Syntax: `domain.action` (`project.read`, `user.create`, `server.shutdown`).
- Kurzform: `project` meint `project.*`. `*` matcht genau ein Segment (`namespace.*.articles.read`).
- Gültige Domänen: `user`, `project` (plus `reload`, `backup`, `regenerate-search`, `snapshot`), `team`, `org`, `audit.read`, `llm.chat`, `namespace…`.
- Header: `X-Admin-Token: …` oder `Authorization: Bearer …`.
## Abnahme über den Version-Endpunkt
<Steps>
1. Server starten und warten, bis das Config-Dump im Log steht.
2. Version abfragen — antwortet sie, stimmen Port und Prefix:
```bash title="Version prüfen"
curl http://localhost:8080/api/v1/version
# {"version":"0.0.0"}
```
3. Bei Fehlern zuerst `DB_DIAL`, `DB_PREFIX` und `API_PORT` prüfen (Tabelle oben).
</Steps>
Weiter: [Betrieb und Beobachtung](/server/betrieb/) zeigt Profile und Metriken. [Sicherheit](/server/sicherheit/) erklärt Token-Arten und Ratelimit.
Verifiziert am `68b42785` 2026-10-15 (tibi-server); Starter-Compose am `e3f92ebf` 2026-10-14 (tibi-svelte-starter); Go-Version aus `go.mod`.
+104
View File
@@ -0,0 +1,104 @@
---
title: 'Schnittstellen im Überblick'
description: 'REST, SSE, MCP, OpenAPI: was wofür gilt und wo die Referenz liegt.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Der **tibi-server** spricht vier Sprachen: REST, SSE-Streams, MCP und OpenAPI. Alle hängen unter `/api/v1` (außer `/metrics`). Quelle: `internal/controller/setup.go`.
```text title="API-Baum im Überblick"
/api/v1
├── System: /login /token/_scopes /llm/* /audit /admin/* /version /mcp
├── Projekt: /_/:project/:collection … /_/:project/_actions/:action
│ /_/:project/_/assets/… /_/:project/_/admin/…
└── Schema: /_openapi/:project
/metrics (Prometheus, außerhalb)
```
## REST: System gegen Collection
<Tabs>
<TabItem label="System-Endpunkte">
| Methode | Pfad | Wofür |
|---|---|---|
| `GET` | `/api/v1/version` | Lebenszeichen (`{"version":"…"}`) |
| `POST` | `/api/v1/login`, `/api/v1/login/impersonate` | JWT holen / als Admin schlüpfen |
| `GET` | `/api/v1/token/_scopes` | Gültige PAT-Scopes auflisten |
| `POST` / `GET` | `/api/v1/llm/chat`, `/api/v1/llm/providers` | Chat (ohne Secrets) / Provider-Namen |
| `GET` | `/api/v1/audit` | Audit-Logs (`audit.read`) |
| `GET` / `POST` | `/api/v1/admin/embedding/status`, `/api/v1/admin/shutdown` | Embedding-Status / Stop (`server.shutdown`) |
</TabItem>
<TabItem label="Collection-Endpunkte">
| Methode | Pfad | Wofür |
|---|---|---|
| `GET` / `POST` | `/_/:project/:collection` | Suchen / anlegen |
| `GET` | `/_/:project/:collection/_count`, `/_facet`, `/_queue` | Zählen / Facetten / Warteschlange |
| `GET` / `PUT` / `DELETE` | `/_/:project/:collection/:id` | Lesen / ersetzen / löschen |
| `PUT` / `DELETE` | `/_/:project/:collection` (ohne ID) | Bulk-Update / Bulk-Delete |
| `GET` | `/_/:project/:collection/:id/:field`, `…/:field/*path` | Einzelnes Feld / Datei |
| `GET` / `POST` | `/_/:project/_actions/:action` | Hook-Aktion ohne Collection |
| `GET` | `/_/:project/_/assets/:name/*path` | Statische Assets |
Trick für lange Filter: `POST` mit Header `X-HTTP-Method-Override: GET` — der Body wird zur Query.
</TabItem>
<TabItem label="Projekt-Admin">
| Methode | Pfad | Recht |
|---|---|---|
| `POST` | `/_/:project/_/admin/reload` | `project.reload` |
| `POST` / `GET` | `/_/:project/_/admin/regenerate-search`, `…/search-worker/status` | `project.regenerate-search` |
| `GET` | `/_/:project/_/admin/embedding/status` | Projekt-Admin |
| `GET` / `POST` / `DELETE` | `/_/:project/_/admin/backup…` | `project.backup` |
| `GET` / `POST` / `DELETE` | `/_/:project/_/admin/snapshots…` | `project.snapshot` |
</TabItem>
</Tabs>
Alle Antworten sind gzip-komprimiert, Body-Limits kaskadieren (Server → Projekt → Collection). Erstellen meldet `201`, Löschen `204` — außer das Projekt will Legacy-Codes.
## SSE (Echtzeit und Fortschritt)
- Der Realtime-Hub ist ein In-Memory-Pub/Sub **je Projekt**: benannte Kanäle, fester Ringpuffer (Standard 256), Nachhol-Optionen (`LastN`, `MaxAge`, TTL).
- Volle Kanäle werfen wahlweise Alt oder Neu weg — langsame Leser blockieren nie den Server.
- **Kein fertiger Abo-Endpunkt:** Der Server liefert keine HTTP-Route für Collection-Abos. Du erreichst den Hub nur über Hooks (`channel.send` / `channel.subscribe`) — typisch als eigene Action (`/_/:project/_actions/:action`), die du selbst als SSE auslieferst.
- `text/event-stream` liefert der Server fertig nur an zwei Stellen: `POST /api/v1/llm/chat` (Chat-Streaming) und Backup-Analyse-Download (`.../admin/backup/analyze-download`). Snapshot-Streaming ist dagegen Datei-Download mit RAM-Begrenzung, kein SSE.
- Backup- und Snapshot-Läufe melden Fortschritt als Event-Stream. Bricht der Stream ab, war es das Netzwerk — der Lauf selbst geht weiter.
## MCP (KI-Werkzeuge)
| Fakt | Wert |
|---|---|
| Transport | Streamable-HTTP, JSON-RPC 2.0 |
| Route | fest `POST` + `GET /api/v1/mcp` — ein gesetztes `mcp.route`/`MCP_ROUTE` wird ignoriert (Routing hart in `setup.go`) |
| Auth | nur PAT via `X-Auth-Token` — kein JWT, kein Admin-Token |
| Rechte | jede Prüfung nutzt die gleichen Gates wie REST (nichts kopiert) |
| Ergebnis-Deckel | `mcp.maxResultItems` (Standard 100) |
| Admin-Werkzeuge | nur mit `mcp.allowAdminActions: true`, weiter pro Scope begrenzt |
```bash title="MCP-Tools auflisten (describe)"
curl -X POST http://localhost:8080/api/v1/mcp \
-H "X-Auth-Token: tibi_pat_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
## OpenAPI-Generator
`GET /api/v1/_openapi/:project` erzeugt die Spezifikation **deines** Projekts frisch aus Config und Collections. Nutze sie für Client-Generierung und als Klick-Referenz. Die Version oben (`/api/v1/version`) sagt dir, ob Server und Spec zusammenpassen.
## Welche Referenz wofür
| Frage | Antwort liegt in |
|---|---|
| Wie heißt Config-Schlüssel X? | [Server-Konfiguration](/server/konfiguration/) |
| Wer darf Endpunkt Y? | [Sicherheit](/server/sicherheit/) (Tokens, Scopes) |
| Läuft der Server gesund? | [Betrieb und Beobachtung](/server/betrieb/) (Version, Metriken) |
| Wie frage ich Daten ab? | Projekt-Seiten ab [Begriffe](/einstieg/begriffe/) |
| Was ist Tibi überhaupt? | [Was ist Tibi?](/einstieg/was-ist-tibi/) |
Verifiziert am `68b42785` 2026-10-15 (tibi-server).
+82
View File
@@ -0,0 +1,82 @@
---
title: 'Sicherheit'
description: 'Tokens, CORS-Kaskade, Ratelimit, Pfad-Regeln, Isolation als Betreiber-Überblick.'
---
import { Aside, Tabs, TabItem, Steps, Card, CardGrid, Badge, FileTree } from '@astrojs/starlight/components';
Du sicherst den **tibi-server** auf vier Ebenen: Tokens, CORS, Ratelimit und Dateipfade plus Prozess-Isolation. Quelle ist der Code (`internal/config`, `internal/ratelimit`, `internal/suexec`, `internal/controller/setup.go`).
## Token-Arten
<Badge text="X-Admin-Token" variant="danger" /> <Badge text="X-Auth-Token" variant="tip" /> <Badge text="Token" variant="note" /> <Badge text="Bearer" variant="caution" />
| Art | Header | Wofür | Scope-Syntax |
|---|---|---|---|
| Server-Admin-Token | `X-Admin-Token` oder `Authorization: Bearer` | Betrieb: Reload, Backup, Snapshots, Shutdown (`server.shutdown` nur für Stop) | `domain.action`, Kurzform (`project` = `project.*`), `*` je Segment |
| Benutzer-JWT | `X-Auth-Token` | Anmeldung per `/api/v1/login`; Lebensdauer via `API_ACCESS_TOKEN_LIFETIME` (Standard `60m`) | Rollen + Collection-Rechte |
| Personal Access Token (PAT) | `X-Auth-Token` (`tibi_pat_…`) | Dauerhafte Maschinen-Tokens, immer aktiv | gleiche Syntax wie Admin-Tokens; es gilt **Token-Recht UND Besitzer-Recht** |
| MCP | `X-Auth-Token` (nur PAT) | KI-Werkzeugzugriff über `/api/v1/mcp` | PAT-Scopes, Admin-Aktionen nur mit `mcp.allowAdminActions: true` |
Gültige Domänen: `user`, `project` (`read/create/update/delete`, `reload`, `backup`, `regenerate-search`, `snapshot`), `team`, `org`, `audit.read`, `llm.chat`, `namespace.<ns>.<collection>.<aktion>`.
```text title="Scope-Abgleich (Beispiel)"
Anfrage-Recht: namespace.shop.artikel.read
Token-Scope: namespace.shop → passt (Kurzform)
Token-Scope: namespace.*.artikel.read → passt (Wildcard)
Besitzer darf es nicht → trotzdem verboten (UND-Regel)
```
<Aside type="caution" title="Entfernt: TOKENS_ENABLED">
Ein Flag `TOKENS_ENABLED` gibt es nicht mehr. PATs sind Kernfunktion und immer an. Wer alte Compose-Dateien mit diesem Flag findet, löscht es ersatzlos.
</Aside>
## CORS-Kaskade
Jeder Request bekommt genau eine CORS-Config — vom Spezifischen zum Allgemeinen:
```text title="CORS-Kaskade"
/_actions/<name> → /<collection> → / → "" → keine (Browser blockt)
```
- Die Kaskade steht pro Projekt (`CORSMap`), nicht global. Der `cors:`-Abschnitt der Server-Config ist die **Basis**, Projekt- und Collection-Werte mergen feldweise dazu (`merge: true` hängt an, `merge: false` ersetzt). Globales CORS ist auskommentiert — ohne Projekt-Kontext greift gar kein CORS.
- Action-Endpunkte (`/_actions/…`) haben eigene Einträge — prüfe sie separat.
- Preflight ohne echte Route antwortet `404` (kein offenes `OPTIONS`-Loch).
## Ratelimit (Login)
Der Login-Limiter bremst Fehlversuche mit exponentiellem Backoff — je IP **und** je Benutzername:
| Schlüssel | Standard | Env |
|---|---|---|
| Erste Verzögerung | `1s` | `RATELIMIT_LOGIN_INITIAL_DELAY` |
| Maximale Verzögerung | `60s` | `RATELIMIT_LOGIN_MAX_DELAY` |
| Zähler-Reset nach Ruhe | `15m` | `RATELIMIT_LOGIN_RESET_AFTER` |
| Ganz abschalten | an (`true`) | `RATELIMIT_ENABLED=false` (nur Notfall) |
Erfolg setzt beide Zähler zurück. Gebremste Versuche antworten mit **429** plus `Retry-After`-Header (Sekunden) — warte sie ab statt zu hämmern. Das Limit gilt **nur für Login**, nicht global.
## Pfade und suexec-Isolation
| Schalter | Wirkung |
|---|---|
| `security.allowAbsolutePaths` (`SECURITY_ALLOWABSOLUTEPATHS`) | Absolute Dateipfade erlauben — sonst verboten |
| `security.allowUpperPaths` (`SECURITY_ALLOWUPPERPATHS`) | `..`-Pfade erlauben — sonst verboten |
| suexec (kein Schalter) | Hooks und Jobs laufen unter der Projekt-UID/GID in eigenen Thread-Pools |
Der Starter setzt beide Pfad-Schalter lokal auf `"true"`. In Produktion lässt du sie aus, außer du weißt genau warum.
## Checkliste für den Go-Live
<Steps>
1. `jwtSecret` gesetzt (lang, zufällig) und `secureCookies: true` (HTTPS).
2. Admin-Tokens minimal vergeben: ein Token, ein Zweck, enge Scopes.
3. `RESPONSE_ERROR_STACK` aus — keine Stacktraces an Clients.
4. Pfad-Schalter aus, außer nötig. Ratelimit an.
5. `sentry.dsn` gesetzt, `/metrics` nur intern erreichbar.
6. CORS je Collection geprüft — keine `*`-Origins mit Credentials.
</Steps>
Config-Schlüssel stehen unter [Server-Konfiguration](/server/konfiguration/). Laufende Kontrolle unter [Betrieb und Beobachtung](/server/betrieb/).
Verifiziert am `68b42785` 2026-10-15 (tibi-server).
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more