---
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)