--- title: 'Änderungen und Sicherung' description: 'Audit-Log lesen, Snapshots nutzen, Backup und Restore ausführen.' --- 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. Protokolliert Änderungen — wer, wann, vorher/nachher. Frieren Datenstände im Ordner `snapshotPath` ein. Voll-Export als ZIP für Umzug und Ernstfall. ## 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 | 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). ## Verlauf im Admin Du musst nicht per API lesen: Der Admin zeigt den Verlauf pro Eintrag (letzte 50 Änderungen mit Vorher/Nachher-Vergleich und Wiederherstellen einzelner Felder) sowie global als Audit-Log. Details zur Bedienung — Verlaufspanel, Diff-Ansicht, Wiederherstellen-Dialog, Seitenleiste — stehen unter [Eintrags-Editor](/admin/eintrags-editor/). ## 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 | ### Snapshot-Config-Felder Alle Felder aus `snapshot_config.go:11-58` — nur diese Namen verwenden: | Feld | YAML-Name | Bedeutung | |---|---|---| | Name | `name` | Eindeutige Profil-Kennung (Pflicht, sonst `snapshot config: name is required`) | | Beschreibung | `description` | Freitext, optional | | Sammlungen | `collections` | `[]` = alle; String-Kurzform `"content"` = `include`; Objektform `{name, mode}` (`include` oder `exclude`) — gemischt `include` + `exclude` ist ein Validierungsfehler | | Medien | `includeMedia` | Datei-Felder per Hardlink sichern (Standard: `true`) | | Deduplizierung | `collectionDedup` | SHA256-Dedup identischer Collection-Exporte über `.store/collections/` per Hardlink — nur für große, selten geänderte Sammlungen | | Modus | `mode` | `""`/`json` = Platte (Standard, implementiert); `db`/`both` = MongoDB-`$out`-Kopie (Zukunft) — Medien immer Hardlink, egal welcher Modus | | Aufbewahrung | `retention: { maxCount, maxAge, maxSizeBytes }` | Auto-Prune-Regeln, alle optional | | Zeitplan | `schedule` | Cron-Ausdruck; leer = nur manuell per Trigger-API | | Team-Rechte | `permissions` | Liste reiner Berechtigungsnamen — Phase 2, aktuell gilt `AllowSnapshots` | | An/Aus | `enabled` | `false` = Scheduler überspringt das Profil; manueller Trigger geht trotzdem | ```yaml title="Snapshot-Profil — alle Felder (Beispiel)" snapshots: - name: redaktionell description: "Täglich, ohne Systemdaten" collections: - content # Kurzform = include - name: ssr # Objektform mode: exclude includeMedia: true collectionDedup: false mode: json retention: { maxCount: 14, maxAge: "336h", maxSizeBytes: 10737418240 } schedule: "0 12 * * *" permissions: [] enabled: true ``` ### Snapshot-Routen Alle Routen aus `setup.go:436-495` (`GET` liest, `POST` löst aus, `DELETE` löscht): | Methode | Pfad (hinter `/api/v1/_/:project/_/admin`) | Zweck | |---|---|---| | `GET` | `/snapshots/configs` | Alle Profile aus der Projekt-YAML lesen (`AdminSnapshotConfigs`) | | `POST` | `/snapshots/configs/:name/trigger` | Profil manuell auslösen — geht auch bei `enabled: false` | | `GET` | `/snapshots` | Alle Einträge auflisten (Scannt das `entries`-Verzeichnis) | | `GET` | `/snapshots/download/:file` | Datei-Download (vor `:id` registriert, damit `download` kein `:id` matcht) | | `GET` | `/snapshots/:id` | Einzelnen Eintrag ansehen | | `DELETE` | `/snapshots/:id` | Eintrag löschen | | `POST` | `/snapshots/:id/restore?merge=1&collections=a,b` | Zurückspielen (`merge=1` = zusammenführen; `collections` = Auswahl) | | `GET` | `/snapshots/:id/download` | Eintrag als ZIP laden | | `GET` | `/snapshots/stats` | Belegung prüfen | | `POST` | `/snapshots/prune` | Retention-Aufräumen manuell anstoßen | 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 `allowSnapshots: true`. Übe das Zurückspielen **nur mit Testdaten** — es überschreibt den Live-Stand. ### Backup-Routen Alle Routen aus `setup.go:398-432`: | Methode | Pfad (hinter `/api/v1/_/:project/_/admin`) | Zweck | |---|---|---| | `GET` | `/backup/analyze-download?collections=a,b` | Schätzung (Dokument-/Medien-Zahlen) **ohne** Voll-Export — vor dem Download prüfen | | `GET` | `/backup` | Voll-ZIP erzeugen und laden (`?collections=` schränkt ein, `?sse=1` streamt) | | `GET` | `/backup/download/:file` | Fertiges Temp-ZIP aus dem SSE-Modus laden (**One-Time-Use**: zweiter Request = `404`) | | `POST` | `/backup?sse=1` | ZIP hochladen und **analysieren** → antwortet mit `restoreToken` | | `POST` | `/backup/:restoreToken?collections=…&merge=true&sse=1` | **Restore** aus dem analysierten Temp-Verzeichnis ausführen | | `DELETE` | `/backup/:restoreToken` | Temp-Daten zum Token manuell aufräumen | ### Restore-Token-Lebenszyklus `POST /backup` erzeugt Token plus Temp-Verzeichnis (`admin_backup.go:204-234`), `POST /backup/:restoreToken` verbraucht es, `DELETE` räumt auf. 1. **Analyse:** ZIP per `POST /backup` hochladen — Antwort enthält `restoreToken`, `sourceNamespace`, `createdAt`, `expiresAt` (`admin_backup.go:378-384`). Mit `?sse=1` kommt der Fortschritt als Stream (`init` → `done`, jeweils mit `restoreToken`). 2. **Prüfen:** `collections`-Analyse in der Antwort lesen (Quelle gegen Ziel abgleichen, ggf. `?collections=` für Teil-Restore vormerken). 3. **Restore:** `POST /backup/:restoreToken` ausführen — bei Erfolg landet `restoredCollections`/`totalDocuments`/`mediaFiles` in der Antwort, alles wird als Audit-Action `backup/restore` protokolliert. Danach ist das Token **verbraucht** (Eintrag wird vor dem Restore aus dem Store entfernt). 4. **Cleanup:** Läuft der Restore ohne `sse`, löscht der Server das Temp-Verzeichnis selbst (`os.RemoveAll`). Bei Abbruch oder abgelaufenem Token per `DELETE /backup/:restoreToken` aufräumen — Antwort `404 restore token not found or already cleaned up` heißt: schon weg. Abgelaufene Tokens räumt zusätzlich ein Hintergrund-Job weg. ### Rechte und Auth-Reihenfolge | Ebene | Schalter | Beleg | |---|---|---| | Server-Token | Scope `project.backup` / `project.snapshot` (`scope.go:53-55`) | `X-Admin-Token` oder `Authorization: Bearer` | | Projekt-Token | `allowBackup` / `allowSnapshots` in der Projekt-Config (`project.go:118-120`) | Nur mit passendem Flag | | JWT-Login | Rolle `AdminRole` plus Scope-Check (`EnforceScope`) | Persönliche Tokens bleiben durch ihre Scopes begrenzt | Der Check läuft in `admin_auth.go:20-74` **immer in dieser Reihenfolge**: 1. Server-Token mit passendem Scope (`config.GetAdminTokenInfo`). 2. Projekt-Token mit passendem Flag (`AllowBackup` / `AllowSnapshots` per `ConstantTimeCompare`). 3. JWT-Login mit Admin-Rolle plus Scope-Prüfung — sonst `403 permission denied`. ## Backup und Restore Backups sind der Voll-Export für Umzug und Desaster-Fall — im Admin unter `/projects/:id/backup`: 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. | Unterschied | Snapshot | Backup | |---|---|---| | Wofür | Schneller Rücksprung im Alltag | Umzug, Desaster, Weitergabe | | Wo | `snapshotPath` auf dem Server | ZIP-Datei bei dir | | Recht | `allowSnapshots` | Projekt-Admin plus `AllowBackup` | | Fortschritt | Ja (Stream) | Ja (SSE-Stream) | ## 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 `allowSnapshots` | Flag `allowSnapshots` am Projekt-Token setzen (siehe [Rechte und Auth-Reihenfolge](#rechte-und-auth-reihenfolge)) | | Backup-Button fehlt | Kein Projekt-Admin oder `AllowBackup` fehlt | Projekt-Admin-Rolle bzw. Flag `AllowBackup` am Projekt-Token prüfen (siehe [Rechte und Auth-Reihenfolge](#rechte-und-auth-reihenfolge)) | | `400 missing restoreToken` | Restore ohne Token aufgerufen | Erst `POST /backup` (Analyse), Token aus der Antwort nehmen | | `404 restore token not found or expired` | Token verbraucht, aufgeräumt oder TTL abgelaufen | Analyse erneut hochladen, neues Token holen | | `403 invalid entry id` / `404 snapshot not found` | Snapshot-Restore mit falscher/gelöschter ID | ID aus `GET /snapshots` neu lesen | | `snapshot config: name is required` | Profil ohne `name` | `name` in der YAML ergänzen | | Restore zeigt keine Wirkung | Cache alt | Reload plus `?clear=1` (siehe [Veröffentlichen](/projekt/deploy/)) | Wie ein Redakteur eine Sicherung einspielt: [Backup einspielen](/anwender/backup-einspielen/). Verifiziert am e3f92eb 2026-10-17 (tibi-svelte-starter, config.yml audit- und snapshots-Blöcke), am 68b4278 2026-10-17 (tibi-server, audit-, backup- und snapshot-docs), am 2b091d8 2026-10-17 (tibi-admin-nova, test-pläne 43 und 44, AuditView, AuditHistoryPanel, AuditDiff, AuditRestoreDialog, AuditSidebarHistory)