# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Status dieses Repositories **Phase 0–23 sind implementiert. Bei Phase 7 (Proxmox) fehlt nur noch die Ausführung auf echter Hardware** — der Sicherungs- und Wiederherstellungsweg eines Gasts läuft vollständig durch, aber kein Test belegt, dass die wiederhergestellte Maschine **startet**. **Bei Phase 5 und 6 bleibt nur die Ausführung auf echten Systemen offen:** Der Windows-Dienst und die systemd-Einheit sind geschrieben und übersetzen, wurden aber nie geladen. Vorhanden sind Control-Plane-API, Health-Engine, Logging, PostgreSQL mit Migrationsframework, Argon2id-Anmeldung, TOTP-MFA mit Replay-Schutz, RBAC (7 Rollen / 34 Berechtigungen), Append-only-Audit, Brute-Force-Schutz, Login-UI, Dev-Umgebung und CI. Dazu die Repository Engine: inhaltsadressierte Chunks mit Deduplizierung, atomares Commit-Protokoll, Integritätsscan, Katalog-Wiederaufbau ohne Datenbank, gehärteter Modus. Dazu das versionierte Container-Format mit Export/Import und die Backup Engine mit inhaltsabhängigem Chunking, Deduplizierung trotz Verschlüsselung, zstd, AES-256-GCM und Streaming-Pipeline. Dazu der Agent mit Aufnahme, Betriebstoken, Lebendmeldung, Dateierfassung, Voll- und Zusatzsicherung, Wiederherstellung sowie der **Auftragsübermittlung vom Server** (Phase 5): Der Agent holt Aufträge ab, führt sie aus und meldet Fortschritt und Ergebnis zurück. **Die Windows-Dienstanbindung ist vollständig geschrieben (svc.Run mit Steuerbefehlen), übersetzt für Windows und ist `vet`-sauber — aber nie ausgeführt worden.** `make cross-build` prüft die Übersetzbarkeit für windows/amd64, linux/amd64 und linux/arm64 bei jedem Lauf. Dazu die Provider-Schnittstelle und der Proxmox-Provider samt Bestandsverwaltung, beiden Zugriffswegen auf die Sicherungsarchive (Dateizugriff und SSH), Sicherung eines Gasts über den Scheduler und Wiederherstellung auf einen Knoten — **dessen verpflichtender E2E-Meilenstein bleibt jedoch offen, weil kein echter Verbund zur Verfügung stand**, siehe `docs/proxmox.md`. Dazu Scheduler und Ausführungsschleife (Phase 8), die Recovery Engine mit Vorabprüfung, Prüfpunkt und Fortsetzung (Phase 9) sowie Prüfung und Recovery Assurance mit fünf Prüfarten, automatischem Wiederherstellungstest, Einstufung und Bewertung (Phase 10). Dazu Unveränderlichkeit mit gemessener Durchsetzungsstufe, Aufbewahrungsschutz, Legal Hold und Aufbewahrungsregeln (Phase 11). Dazu die Weboberfläche mit Übersicht, Wiederherstellungspunkten, Bestandslisten und benannten Lücken (Phase 12) sowie Kennzahlen mit zwölf Diagrammen über sieben Zeiträume (Phase 13) und das Meldungswesen mit zehn geprüften Regeln, Selbstauflösung sowie Zustellung per E-Mail und Webhook (Phase 14). Dazu das Security Center mit acht geprüften Bereichen, vollständigen Befunden und Verlauf der Bewertung (Phase 15) sowie die Ransomware-Heuristik mit sechs Signalen gegen einen robusten Basiswert je Kette, die meldet und niemals handelt (Phase 16). Dazu neun Berichte in CSV, JSON und selbst geschriebenem PDF (Phase 17) und Disaster Recovery mit Konfigurationssicherung im Repository, deren vier Szenarien real durchgespielt sind (Phase 18). Dazu die Härtung mit Abhängigkeits-, Statik- und Geheimnisprüfung, elf gefahrenen Angriffsarten, SSRF-Schutz, Zielverzeichnisprüfung, allgemeinem Ratenbegrenzer und TLS (Phase 19). Dazu die Leistungsmessung mit sieben gefahrenen Szenarien und benannten Messgrenzen (Phase 20) sowie das Chaos Testing mit neun Störungen gegen eine prüfbare Definition von „kontrolliert" (Phase 21). Dazu die eingefrorenen Verträge für API, Migrationen, Backup-Format und Repository-Protokoll, der erstmals gefahrene Upgrade- und Rollback-Test über Bestandsdaten sowie der vollständige Durchlauf mit Disaster-Recovery-, Security- und Performance-Review (Phase 22). Dazu das Auslieferungspaket für drei Zielplattformen samt Oberfläche, Migrationen und Dokumentation, die fünf fehlenden Auslieferungsdokumente und die Änderungsliste (Phase 23). Die vier Spezifikationsdokumente sind die verbindliche Quelle der Wahrheit: | Datei | Inhalt | | --- | --- | | `PROMPT.md` | Master-Prompt (152 Abschnitte, deutsch) — Produktphilosophie, Technologiewahl, Funktionsumfang, verbindliche Entwicklungsregeln | | `SYNCOVA_ARCHITECTURE.md` | Servicezuschnitt, Provider-/Agent-Architektur, Backup-Format, Repository-Commit-Protokoll | | `SYNCOVA_DATABASE.md` | Vollständiges PostgreSQL-Schema (Tabellen, FKs, empfohlene Indizes) | | `SYNCOVA_API.md` | REST-API-Vertrag unter `/api/v1` (Response-Hülle, Endpunkte, Idempotenz, Pagination) | | `SYNCOVA_IMPLEMENTATION_PLAN.md` | Phasen 0–23 mit Exit-Kriterien und Akzeptanz-Testmatrix | Bei Widersprüchen gilt: `PROMPT.md` ist das Produktziel, die drei `SYNCOVA_*`-Dokumente sind dessen technische Konkretisierung. ## Befehle ```bash make dev-env # .env mit zufälligem DB-Passwort UND Verschlüsselungsschlüssel make dev-up # startet PostgreSQL (braucht laufenden Docker-Daemon) make migrate-up # wendet Migrationen an make create-admin USERNAME=admin # erster Administrator (kein Standardkonto im Code) make run-api # API auf 127.0.0.1:8080 make web-install # einmalig; danach: make web-dev (Oberfläche auf :5173) make check # alle Prüfungen wie in der CI make help # alle Ziele ``` Einzelnen Go-Test ausführen: `go test -race -run TestLoadRejectsWildcardCORSOrigin ./packages/platform/config` Einzelnen Frontend-Test: `cd apps/web && npx vitest run src/api/client.test.ts` **Wichtig für Go-Aufrufe:** niemals `./...` verwenden — `apps/web/node_modules` enthält Fremd-Go-Code, der sonst in Tests, Lint und Schwachstellenprüfung landet. Das Makefile filtert ihn über `GO_PACKAGES` heraus; bei direkten Aufrufen `$(go list ./... | grep -v '/node_modules/')` nutzen. Tests in `packages/platform/config` rufen zuerst `isolateEnvironment(t)` auf. Das ist notwendig, weil das Makefile die lokale `.env` exportiert und die Tests sonst je nach Entwicklungsrechner unterschiedlich ausgingen. **Beim Testen von Servern:** `go run` startet ein Kindbinary — ein `kill` auf die Shell-PID trifft nur den Wrapper und lässt den Port belegt. Für Tests `make build` nutzen und `./bin/syncova-api` direkt starten. ## Was Syncova ist Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform für Proxmox VE, Windows, Linux, physische Systeme sowie Dateien/Ordner. VMware, Hyper-V, Kubernetes, M365 sind **nicht** Teil von V1, die Architektur muss sie aber später aufnehmen können. Zentrales Produktprinzip, das nahezu jede Designentscheidung erklärt: > Ein Backup gilt erst als vertrauenswürdig, wenn Integrität geprüft und Wiederherstellbarkeit nachgewiesen wurde. ## Technologie-Stack (vorgegeben) - **Backend:** Go — Control Plane, Backup Engine, Agents, Repository Service, API, Scheduler, Monitoring. Rust nur später für begründete Low-Level-Komponenten. - **Frontend:** React + TypeScript, responsive (Desktop/Laptop/Tablet), Dark Mode. - **Datenbank:** PostgreSQL, ausschließlich Control Plane. Migrationstool: golang-migrate oder Atlas. - **Kommunikation:** REST extern, gRPC intern wo sinnvoll, WebSocket/SSE für Live-Status (`/api/v1/events/stream`). - **Deployment:** Linux-Server / VM / Backup-Appliance; Paketierung so, dass Container später möglich sind. ## Repository-Layout ```text apps/ api/ cmd/{syncova-api, syncova-migrate, syncova-admin, syncova-repo, syncova-dr, syncova-bench, syncova-proxmox}, internal/httpapi web/ React + TypeScript (src/api, components, features, navigation, styles, types) agent/ cmd/syncova-agent (Dienstadapter je Plattform) worker/ leer — ab Phase 4 packages/ platform/ config, logging, database, health, crypto, totp (Querschnitt) auth/ Identität, Sitzungen, RBAC, MFA audit/ Append-only-Protokoll sicherheitsrelevanter Handlungen backupexecutor/ verbindet die Ausführungsschleife mit der Backup Engine jobs/ Sicherungsaufträge: Modell, Prüfung, PostgreSQL-Speicher, Schleife providers/ herstellerneutrale Virtualisierungsschnittstelle + proxmox/ hypervisor/ eingerichtete Virtualisierungsumgebungen: Zugangsdaten, Bestand, Gastwiederherstellung ransomware/ statistische Auffälligkeitserkennung: Basiswert, sechs Signale reports/ neun Berichte, formatunabhängiges Modell, CSV/JSON/PDF disasterrecovery/ Konfigurationssicherung im Repository, Wiederaufbau der Control Plane platform/netguard/ Sperre interner Zieladressen (SSRF) platform/secretscan/ Geheimnissuche im Quellbestand benchmark/ Messmodell mit Ressourcenerfassung chaos/ Definition und Prüfung kontrollierter Störungsergebnisse recovery/ Vorabprüfung, Sitzungen mit Prüfpunkt, Wiederherstellungsschleife scheduler/ Zeitpläne, Wartungsfenster, Warteschlange, Wiederholungen repository/ Chunk-Ablage, Manifeste, Commit-Protokoll, Katalog, Integritätsscan verification/ Prüfmaschine, Wiederherstellungstest, Einstufung, Bewertung retention/ Aufbewahrungsregeln, Vorschau, Anwendung, Löschschutz metrics/ Zeitreihen, Diagrammkatalog, Kennzahlenerfassung alerting/ Regelwerk, Auswertung, Meldungen, Zustellung security/ Sicherheitsprüfungen, Befunde, Security Score backupformat/ portabler Container: Header, Abschnitte, Footer, Versionsverhandlung backupengine/ Chunking, Kompression, Verschlüsselung, Pipeline, Wiederherstellung agent/ Dateierfassung, Client, Betriebsschleife (plattformunabhängig) agentregistry/ Serverseitige Agent-Verwaltung: Aufnahme, Tokens, Sperre agenttasks/ Auftragsvermittlung zwischen Server und Agent (Pull, Backup und Restore) migrations/ eingebettete SQL-Migrationen (embed.FS), 000001–000013 + checksums.txt (eingefroren) scripts/ build-release.sh + Test auf Vollständigkeit der Auslieferung deployment/ docker-compose für die lokale Umgebung docs/ development.md, architecture.md, security.md, repository.md, backup-format.md, backup-engine.md, agent.md, proxmox.md, scheduler.md, recovery.md, verification.md, ransomware.md, immutability.md, reports.md, disaster-recovery.md, hardening.md, performance.md, chaos.md, agent-tasks.md, agent-installation.md, linux-agent.md, release-candidate.md, installation.md, recovery-runbook.md, security-guide.md, api.md, troubleshooting.md, web-ui.md, metrics.md, alerting.md, security-center.md ``` `packages/` darf `apps/` **nie** importieren — nur so können Agent und Worker später dieselben Querschnittsdienste nutzen. Fachliche Pakete (`backup/`, `repository/`, `crypto/`, `providers/`) kommen ab Phase 2 unter `packages/` dazu. ## Architektur — die nicht-offensichtlichen Zusammenhänge **Die Schichtung ist eine Abhängigkeitsregel, kein Diagramm.** Der Datenpfad lautet: ```text API → Control/Scheduler → Backup Orchestrator → Provider|Agent → Backup Engine → Repository Service → Storage ``` Provider- und Agent-spezifischer Code darf **niemals** in die Backup Engine gelangen. Proxmox liegt hinter dem generischen `VirtualizationProvider`-Interface (`Connect`, `ListVMs`, `GetVMDisks`, `CreateSnapshot`, `ReadChangedBlocks`, `RestoreVM`, …); ein VMware-/Hyper-V-Provider muss sich ohne Änderung der Engine ergänzen lassen. **PostgreSQL ist nicht die Quelle der Wahrheit für Backup-Daten.** Die DB hält Konfiguration, Jobs, Metadaten-Referenzen und Chunk-*Index*-Einträge — niemals Chunk-Payloads. Das Repository muss selbstbeschreibend und ohne die DB rekonstruierbar sein: `Attach Repository → Discover Format → Scan Manifests → Validate Chains → Rebuild Catalog → Restore`. Jede Designentscheidung, die diese Rebuild-Fähigkeit bricht, ist falsch — auch wenn sie bequemer ist. **Der Commit ist ein Protokoll, kein Insert.** Session anlegen → Chunks schreiben → Manifest schreiben → Manifest verifizieren → Completion-Marker atomar setzen → Katalog aktualisieren → Erfolgszustand veröffentlichen. Ein Backup ohne gültigen Completion-Marker ist unvollständig, unabhängig davon, was in der DB steht. **Die Backup-Pipeline ist streaming mit Backpressure:** `Read → Chunk → Hash → Dedup → Compress → Encrypt → Write → Manifest → Commit → Verify`. Vollständige Backups dürfen nie in den RAM geladen werden; zwischen den Stufen gehören begrenzte Worker-Pools, Checkpoints, Cancellation und Retry mit begrenztem exponentiellem Backoff. **Backup-Format ist versioniert** (Header / Manifest / Chunk-Index / verschlüsselte Chunks / Footer mit Manifest-Hash und Completion-Marker) und muss Versionsverhandlung unterstützen. Breaking Changes ausschließlich über Formatversion. ## Verbindliche Entwicklungsregeln Diese stammen aus `PROMPT.md` §§137–141 und `SYNCOVA_IMPLEMENTATION_PLAN.md` §28 und sind nicht verhandelbar: 1. Niemals ein abgeschlossenes Backup vortäuschen; Teilfehler heißen `PARTIAL FAILURE`, nie `SUCCESS`. 2. Integritätsfehler niemals verbergen — keine stillen Fehler, jeder Fehler wird klassifiziert (transient, permanent, integrity, auth, repository, source, network, configuration, security). 3. Backup-Payloads niemals in PostgreSQL. 4. Secrets niemals in Logs, Fehlermeldungen, API-Responses, Frontend oder Klartext-Datenbanken. 5. Autorisierung immer serverseitig prüfen; Frontend-Berechtigungen sind reine Anzeige. 6. Destruktive Aktionen niemals still — immer auditieren, ggf. Step-up-MFA. 7. Keine Fake-Features: Nicht Implementiertes wird als „Not implemented" gezeigt oder deaktiviert. Keine Mock-Daten, keine erfundenen Statistiken in Production. 8. Kein Backup-Feature ohne zugehörigen Recovery-Test freigeben. 9. Vor dem Code: Architektur, Abhängigkeiten, Datenmodelle, APIs, Sicherheitsrisiken, Plan — dann erst implementieren. **Definition of Done** (§131): implementiert, Unit-Tests, ggf. Integrationstests, Fehlerbehandlung, Logging, Security geprüft, UI falls user-facing, API falls relevant, Dokumentation, Migration falls DB betroffen, Monitoring falls relevant, Recovery getestet falls Backup/Recovery betroffen. ## Bereits getroffene Implementierungsentscheidungen Diese Punkte sind im Code verankert und sollten nicht ohne Grund umgeworfen werden: - **Ein Datenbanktreiber für alles.** Verbindungspool *und* Migrationsläufe nutzen `pgx`. Würde man `golang-migrate` die Verbindung selbst öffnen lassen, käme dessen `lib/pq` zum Einsatz, das andere SSL-Modi kennt (`sslmode=prefer` schlägt dort fehl) — dieselbe Konfiguration funktionierte dann je nach Kommando oder nicht. - **Die Antworthülle ist maßgeblich, nicht der HTTP-Status.** `GET /api/v1/health` liefert bei kritischem Zustand 503 *mit* vollständigem `data`-Bericht: Monitoring schlägt an, die Oberfläche kann trotzdem anzeigen, was kaputt ist. Der Frontend-Client wertet deshalb `error` vs. `data` aus, nicht `response.ok`. - **Liveness prüft keine Abhängigkeiten.** Sonst löste eine kurz nicht erreichbare Datenbank einen Prozessneustart aus und verschlimmerte den Ausfall. Nur Readiness bewertet kritische Komponenten. - **Fail Secure in der Health Engine:** eine Prüfung ohne gemeldeten Status, eine Zeitüberschreitung oder ein Panic gelten als `critical`, niemals als gesund. - **Migrationen laufen nie beim Dienststart.** `syncova-api` prüft nur, ob der Schemastand zur Programmversion passt, und verweigert sonst den Start. `down` geht immer nur einen Schritt und verlangt in der Produktion `SYNCOVA_MIGRATE_CONFIRM_DOWN=yes`. - **Secret-Redaction hängt an `slog.HandlerOptions.ReplaceAttr`** — dem einzigen Ort, an dem sie nicht vergessen werden kann. Verbindungszeichenketten gehen nur über `RedactedConnectionString()` nach außen. - **Correlation ID vom Client wird nur übernommen, wenn sie eine gültige UUID ist**; sonst wird sie verworfen, damit keine fremden Zeichenketten in die Logs gelangen. ### Phase 1 (Identität und Sicherheit) - **Opake Tokens statt JWT** — Grund ist die sofortige Widerrufbarkeit; ein JWT bliebe nach Sperre oder Passwortänderung bis zum Ablauf gültig. Gespeichert wird nur der SHA-256-Hash (Argon2id wäre hier nutzlos: 256 Bit Zufall lassen sich nicht erraten). - **TOTP ist selbst implementiert**, verifiziert gegen alle zehn offiziellen RFC-6238-Testvektoren. Der Replay-Schutz (`last_used_time_step`) verhindert, dass ein abgefangener Code im selben 30-Sekunden-Fenster erneut gilt. - **Auth und Berechtigung sind untrennbar** in `protectedHandler` verbunden — ein Endpunkt lässt sich nicht versehentlich ohne Berechtigungsprüfung einbinden. - **Audit ist append-only per Datenbank-Trigger**, nicht nur per Anwendungslogik. Eine Nil-UUID wird vor dem Insert zu NULL — sonst verletzte der System-Akteur der Erstinbetriebnahme den Fremdschlüssel und das Ereignis ginge verloren. - **Weitergeleitete IP-Header werden ignoriert.** Sie sind fälschbar; im Audit stünde sonst eine beliebige Adresse. Erst wenn feststeht, welchem Proxy zu trauen ist, darf sich das ändern. - **Schein-Passwortprüfung bei unbekanntem Konto**, damit die Antwortzeit keine gültigen Anmeldenamen verrät. - **Mitgelieferte Rollen sind unveränderlich**; eine Änderung verschöbe die Bedeutung bestehender Zuweisungen. - **Der letzte Administrator ist geschützt** gegen Löschung, Deaktivierung und Rollenentzug. - **Kein Standardkonto im Code.** Erster Administrator ausschließlich über `syncova-admin create-admin`. ### Phase 2 (Repository Engine) - **Prüffrage für jede Designentscheidung:** Bliebe das Repository nutzbar, wenn Control Server und Datenbank ersatzlos verschwinden? `syncova-repo rebuild` baut den Katalog allein aus den Manifesten auf. - **Chunk-Kennung ist der SHA-256-Inhaltshash.** Daraus folgen Deduplizierung ohne Index und Integritätsprüfung ohne Zusatzdaten. Kennungen werden vor jeder Pfadbildung validiert (Path Traversal). - **Schreiben ist vierstufig:** Temp-Datei im Zielverzeichnis → `fsync` Datei → `rename` → `fsync` Verzeichnis. Ohne Schritt 2 und 4 überlebt eine Datei den Stromausfall unvollständig. - **Das Backup wird erst mit dem Manifest sichtbar** (Schritt 5 von 7). Vorher liegen nur Chunks herum, die ein späterer Lauf wiederverwendet — es entsteht nie ein halbes, scheinbar gültiges Backup. - **Manifest-Kennzahlen stammen immer aus der Session**, nie vom Aufrufer. - **Der Katalog ist nur ein Beschleuniger** und wird bei Verlust, Beschädigung oder fremder Repository-Kennung stillschweigend neu gebaut. - **Prune verlangt die Schreibsperre und bricht bei unlesbarem Manifest ab** — sonst würde auf unvollständiger Grundlage gelöscht. - **Sperren werden nie automatisch gelöst**; `BreakLock` ist ein bewusster manueller Eingriff. - **`DeduplicationRatio()` liefert zwei Werte** (Wert + ob definiert). Bei vollständiger Deduplizierung existiert kein endliches Verhältnis; ein stilles `0` stellte den besten Fall als den schlechtesten dar. Für Anzeigen `SavingsPercentage()` nutzen. ### Phase 3 (Backup-Format) - **Der Footer steht am Ende — das ist der ganze Trick.** Ein abgeschnittener Container hat keinen und wird dadurch zuverlässig als unvollständig erkannt. Erst `Close()` macht einen Container gültig. - **Jeder Abschnitt trägt Typ, Flags, Länge und Prüfsumme.** Eine spätere Version darf Abschnitte ergänzen; eine ältere überspringt unbekannte — **außer** sie tragen `FlagRequired`, dann wird die Verarbeitung verweigert statt Vollständigkeit vorzutäuschen. - **`FlagDeferredDigest` löst das Streaming-Problem:** ein als Datenstrom geschriebener Abschnitt kennt seine Prüfsumme erst am Ende, der Kopf ist da längst geschrieben. Statt Rückspulen (was Pipes und Netzwerkziele ausschlösse) bleibt der Kopf-Digest leer; gesichert wird über Chunk-Hashes im Verzeichnis **und** die Gesamtprüfsumme im Footer. - **Die Gesamtprüfsumme im Footer deckt auch einen ausgetauschten Abschnitt samt passender Einzelprüfsumme auf.** Zusätzlich prüft der Leser die Abschnittszahl — ein entfernter Abschnitt fällt damit auf. - **Export bricht bei fehlendem oder beschädigtem Chunk ab**, statt einen lückenhaften Container zu erzeugen. Das Kommando löscht die Zieldatei dann wieder. - **Import macht das Manifest erst nach allen Prüfungen sichtbar** — ein fehlgeschlagener Import hinterlässt allenfalls Chunks, nie ein scheinbar gültiges Backup. ### Phase 4 (Backup Engine) **Deduplizierung und Verschlüsselung zugleich hat drei Fallen — alle drei sind gelöst, zwei davon erst nach einem fehlgeschlagenen Test:** 1. **Die Chunk-Kennung ist der Hash des Klartextes**, nicht des Geheimtextes. Sonst fänden zwei gleiche Ursprungsblöcke nie zusammen. Abgelegt wird die transformierte Form unter dieser Kennung. 2. **Der Datenschlüssel gehört zum Repository, nicht zum Backup** (`metadata/data-key.json`). Ein Schlüssel je Backup machte Deduplizierung über Backupgrenzen unmöglich — ein späteres Backup verwiese auf Blöcke mit fremdem Schlüssel. *Diesen Fehler hat ein Test aufgedeckt.* 3. **Die GCM-Nonce wird deterministisch aus dem Klartext abgeleitet** (`HMAC(nonceKey, plaintext)[:12]`). Ein Zähler ergäbe für denselben Klartext jedes Mal anderen Geheimtext. Eine Nonce wiederholt sich nur bei identischem Klartext — dann ist der Geheimtext ohnehin gleich; der gefährliche GCM-Fall tritt nicht ein. Weiteres: - **`Chunker.Next()` räumt den vorigen Block erst beim nächsten Aufruf** aus dem Puffer. Würde er das sofort tun, überschriebe das Nachrücken genau den ausgelieferten Bereich — der Aufrufer erhielte stillschweigend verfälschte Daten. *Auch das hat ein Test aufgedeckt.* - **`ChunkReference.StoredDigest`** erlaubt es dem Integritätslauf, verschlüsselte Blöcke **ohne Schlüssel** zu prüfen. - **Reihenfolge: erst komprimieren, dann verschlüsseln.** Umgekehrt liesse sich nichts mehr verkleinern. - **Blöcke, die sich nicht verkleinern liessen, werden unkomprimiert abgelegt** (Marker-Byte). Bei Bildern und Archiven kostete Kompression sonst Platz. - **Messungen nur mit inkompressiblen Daten.** Ein früherer Lauf zeigte 1021-fache Kompression — die Testdaten waren periodisch und die Zahl damit wertlos. - **Bekannte Grenze: das Manifest liegt vollständig im Speicher** (~200 Byte je Blockverweis, also ~1,5 GiB bei 10 TB). Siehe `docs/backup-engine.md`. ### Phase 5 (Agent) — teilweise **Wichtig: Das Exit-Kriterium „ein Windows-System kann gesichert und wiederhergestellt werden" ist NICHT erfüllt.** Der Agent wurde auf macOS entwickelt; `service_windows.go` ist ungeprüft und lässt den Agent im Vordergrund laufen. Auch die Backup-Ausführung durch den Agent fehlt noch — er ist derzeit ein angemeldeter Beobachter. - **Zwei streng getrennte Tokenarten:** Aufnahme-Token (einmalig, 1 h, nur zur Registrierung) und Betriebstoken (dauerhaft, nur agentspezifische Rechte, einzeln widerrufbar). Real geprüft: Agent-Token gegen `/users`, `/roles`, `/audit-events` → 401; Benutzer-Token gegen `/agents/heartbeat` → 401. - **Der Agent bestimmt seinen Namen nicht selbst** — er steht im Aufnahme-Token, sonst könnte er sich als anderes System ausgeben. - **Netzunterbrechung:** Wartezeit wächst 5 s → max. 5 min; Logmeldung nur beim ersten und jedem zehnten Ausfall. Ein **abgelehntes Token** beendet den Agent dagegen — es behebt sich nicht durch Warten. - **Erfassungsprobleme brechen den Lauf nicht ab, werden aber nie verschwiegen** (Exit-Status ≠ 0). Rechtefehler gesondert ausgewiesen. - **Symlinks werden erfasst, aber nicht verfolgt** (Schleifengefahr, fremde Daten). Pfade immer mit Schrägstrich. ### Phase 5 — Auftragsübermittlung an Agenten **Der Agent holt ab; der Server drückt nicht.** Ein Agent steht hinter einer Firewall, oft hinter NAT, und ist vom Server aus nicht erreichbar — jedenfalls nicht ohne eingehende Portfreigabe auf jedem gesicherten System. Die Verbindung geht immer vom Agenten aus, in derselben Richtung wie seine Lebendmeldung. - **Fund — der Server sperrte das Repository für seinen eigenen Agenten.** Phase 8 öffnet es einmal je Lauf und hält die Schreibsperre; bei einer Delegation wartete der Server mit gehaltener Sperre darauf, dass der Agent hineinschreibt. Der Agent meldete `REPOSITORY_UNREACHABLE`, während das Repository einwandfrei dalag. Jetzt wird es nur geöffnet, wenn mindestens eine Quelle **serverseitig** gesichert wird (`hasServerSideSource`); `openRepository` ist in Auflösen und Öffnen getrennt. - **Der Agent braucht Schreibzugriff auf das Repository.** Der Auftrag enthält den Pfad. Auf einem gemeinsamen Server ist das der lokale, bei getrennten Maschinen eine Freigabe. Ein Streaming-Protokoll zum Server wäre die Alternative und ist bewusst nicht gebaut: Es verdoppelt den Datenweg und macht den Control-Server zum Engpass jeder Sicherung. - **Der Lauf wartet synchron auf den Agenten.** Nur einzustellen und den Lauf als erfolgreich zu vermerken ergäbe einen grünen Lauf, während der Agent noch arbeitet — oder bereits gescheitert ist. - **Ein Teilfehler bleibt ein Teilfehler, auf drei Ebenen:** Der Agent meldet ihn als solchen, der Server berichtigt ein widersprüchliches Ergebnis (`TaskResult.Normalize`), und die Datenbank lehnt „erfolgreich mit übergangenen Objekten" per CHECK ab. - **Ein Fehler ist eine Antwort.** Der Agent meldet auch im Fehlerfall zurück — wer schweigt, lässt den Lauf bis zur Frist hängen. Die Rückmeldung nutzt `context.WithoutCancel`, sonst ginge sie beim Beenden des Agenten verloren, obwohl das Ergebnis feststeht. - **Verschlüsselung wird nie stillschweigend weggelassen.** Verlangt der Auftrag sie und fehlt dem Agenten das Schlüsselmaterial, wird abgelehnt statt unverschlüsselt ausgeführt. - **Ein Auftrag je Agent** (Teilindex), **verwaiste Aufträge werden freigegeben, nicht wiederholt** (`AGENT_LOST`, transient — der Agent könnte bereits Blöcke geschrieben haben), **Fortschritt ist zugleich Lebendmeldung** und wird auf fünf Sekunden gedrosselt. - **Ein fremder und ein abgeschlossener Auftrag ergeben dieselbe Antwort** — die Unterscheidung verriete, welche Auftragskennungen existieren. - **Fund — der Agent liess sich fuer Windows gar nicht uebersetzen.** `unix.Statfs` gibt es dort nicht (`repository/health.go`, `recovery/validation.go`). Phase 5 war nie testbar — nicht wegen fehlender Hardware, sondern weil der Code nicht baute. Beide Stellen sind jetzt plattformgetrennt (`capacity_unix.go`/`capacity_windows.go`, `freespace_unix.go`/`freespace_windows.go`); `make cross-build` haengt in `make check` und faengt das in Sekunden. - **Die Windows-Dienstanbindung ist jetzt echt.** Vorher ein Platzhalter, der nur im Vordergrund lief. Jetzt `svc.IsWindowsService()` zur Erkennung, `svc.Run` mit Handler, Zustandsmeldungen (StartPending → Running → StopPending → Stopped) und Behandlung von Stop, Shutdown und Interrogate. Ohne Dienstkontext laeuft derselbe Aufruf im Vordergrund — so dient er fuer Probelauf und Dienstbetrieb. - **Wiederherstellung über den Agenten** (`task_type = 'restore'`) ist umgesetzt. Die drei Huerden gegen versehentliches Ueberschreiben liegen beim Server; der Agent prueft dagegen den **Zielpfad gegen die Systemverzeichnisse seines eigenen Systems** — das kann der Server nicht, er kennt sie nicht. Ein Windows-Agent hat andere Systempfade als ein Linux-Server. - **Fund — der Auftragsinhalt einer Wiederherstellung kam nie beim Agenten an.** `claimedTaskResponse` trug nur das Sicherungsfeld; der Agent bekam einen leeren Repositorypfad und meldete folgerichtig „nicht erreichbar". - **Eine unbekannte Auftragsart wird gemeldet, nicht geraten.** Sie als Sicherung zu behandeln waere die bequeme und gefaehrliche Wahl: Der Server bekaeme ein Ergebnis fuer etwas anderes, als er beauftragt hat. - Nachgewiesen: Auftrag über die API angelegt, vom Agenten abgeholt und ausgeführt (3.500.014 Byte), Wiederherstellungspunkt in der Control Plane, Quelle gelöscht, **vom Agenten** bitgenau zurückgeholt. Wiederherstellung nach `/etc` vom Agenten abgewiesen (`RESTORE_TARGET_FORBIDDEN`, nichts angelegt). Siehe `docs/agent-tasks.md` und `docs/agent-installation.md`. ### Phase 6 — Unix-Eigenheiten **Ein Socket ist kein Fehler.** Bis hierher galt jedes Erfassungsproblem als übergangenes Objekt und machte den Lauf zum Teilfehler — auch ein Socket. Auf einem Linux-System ist das ein Dauerzustand: In `/var/run` und `/tmp` liegen ständig Sockets. Wer `/var` sichert, bekäme bei **jedem** Lauf einen Teilfehler, und nach einer Woche klickt niemand mehr einen an. - **`DiscoveryProblem.IsUnsupportedType` trennt Vermerk von Datenverlust.** Nicht lesbare Datei → Teilfehler (dort fehlen Daten). Socket, FIFO, Gerätedatei → Vermerk, kein Fehler (sie *gehören* nicht ins Backup). `IsPartialFailure()` zählt nur noch `DataLossProblemCount()`; auch der strenge Modus bricht nur bei echtem Verlust ab. - **Verschwiegen wird trotzdem nichts.** `Summary()` nennt übergangene Sonderobjekte auch im Erfolgsfall, und die Meldung benennt den Typ in Worten — „prw-r--r--" sagt einem Betreiber nichts. - **Eine FIFO wird nie gelesen.** Ein Leseversuch ohne Schreiber blockiert für immer; die Erfassung entscheidet anhand des Typs, bevor sie öffnet. Ein Test mit Zeitgrenze hält das fest. - **Fund — die systemd-Einheit hätte nicht funktioniert.** Drei Fehler: `--state` bekam ein **Verzeichnis** statt einer Datei (der Agent hielte sich für registriert und liefe ohne Token); `ReadWritePaths` fehlte das **Repository** (bei `ProtectSystem=strict` hätte er die Quelle gelesen und dann keinen Block ablegen können — der Fehler erst am Ende des Laufs); `RestrictAddressFamilies` fehlte **AF_UNIX** (mit systemd-resolved schlägt die Namensauflösung fehl, während das Netz einwandfrei arbeitet). - **Bekannte Grenze — harte Verknüpfungen werden aufgelöst.** Der Inhalt kommt vollständig zurück und liegt dank Deduplizierung nur einmal im Repository; die Verknüpfung geht verloren (Quelle 2 Verknüpfungen, Ziel 1). Umsetzbar über Inode-Erfassung und `link()` beim Zurückschreiben, griffe aber ins Manifestformat ein. - Nachgewiesen mit Quellverzeichnis aus Dateien, Hardlink, Symlink, FIFO, Unix-Socket und gesperrtem Verzeichnis: Sicherung **erfolgreich** (kein Teilfehler wegen Sonderdateien), Wiederherstellung bitgenau, Symlink und Rechte erhalten, Sonderdateien korrekt nicht im Ziel. Siehe `docs/linux-agent.md`. ### Vertikale Scheibe (§27) — geschlossen `syncova-agent backup` und `restore` verbinden Erfassung, Engine und Repository. Real nachgewiesen: sichern → prüfen → Quelle löschen → wiederherstellen → bitgenau identisch inkl. Rechten und Symlinks. - **Verzeichnisse und Symlinks haben `Reader == nil`** und durchlaufen die Pipeline nicht — sie tragen nur Metadaten, gehören aber ins Manifest (sonst gehen leere Verzeichnisse und Rechte verloren). - **Wiederherstellungsreihenfolge:** Verzeichnisse → Dateien → Symlinks → **Verzeichnisrechte zuletzt, von innen nach außen**. Ein nur lesbares Verzeichnis liesse sich sonst nicht mehr befüllen. - **`validateManifestPath` schützt vor Pfadausbruch.** Ein Manifest kann aus einem fremden Repository stammen; `../../etc/passwd` schriebe sonst irgendwohin. - **Ein nicht leeres Zielverzeichnis wird abgelehnt** (`ErrTargetNotEmpty`), Überschreiben verlangt `--overwrite`. - **Dateien entstehen unter temporärem Namen und werden per `rename` sichtbar** — ein Abbruch hinterlässt keine halbe Datei unter dem echten Namen. - **Ein Lauf mit übergangenen Objekten ist `IsPartialFailure()`** und liefert Exit-Status ≠ 0. `Summary()` sagt dann „TEILWEISE FEHLGESCHLAGEN". ### Phase 6 (Linux Agent) — Zusatzsicherung **Der Gewinn einer Zusatzsicherung ist Zeit, nicht Speicher.** Ein zweiter Volllauf über unveränderte Daten legt dank Deduplizierung ohnehin 0 Byte ab. Gespart wird Lesen, Hashen, Komprimieren, Verschlüsseln: gemessen 190,7 MiB/1 815 ms gegen 4,8 MiB/133 ms bei einer geänderten von 41 Dateien. Wer das verwechselt, hält Zusatzsicherungen für überflüssig. - **Das Manifest einer Zusatzsicherung ist vollständig**, nicht differenziell: unveränderte Objekte tragen die Blockverweise des Elternbackups. Deshalb liest ein Restore genau *ein* Manifest — es gibt keine Kette aufzulösen und keine zu zerreißen —, und das Löschen eines alten Backups kann ein neueres nicht beschädigen. Der Preis ist die Manifestgröße; sie wächst mit dem Bestand, nicht mit der Änderungsmenge. - **`ReusedChunks` auf `BackupSource` prüft die Existenz jedes Blocks**, bevor es ihn übernimmt. Ohne die Prüfung entstünde ein Manifest, das sich als vollständig ausgibt, während seine Daten fehlen — auffallen würde das erst bei der Wiederherstellung. - **Die Zeitstempel-Falle:** Eine Datei, die *während* des Elternlaufs geschrieben wurde, sieht bei sekundengenauer Auflösung unverändert aus. Deshalb gilt jedes Objekt als geändert, dessen Zeitstempel nicht **vor** `parentManifest.StartedAt` liegt. Im Zweifel wird gelesen. - **`BytesReused` steht neben `BytesProcessed`, nicht darin.** Ein deduplizierter Block wurde gelesen und gehasht, ein übernommener nicht einmal geöffnet; ihn mitzuzählen ergäbe eine Leseleistung, die es nie gab. - **Eine Zusatzsicherung ohne Elternbackup schlägt fehl** statt still zur Vollsicherung zu werden. Der Aufrufer glaubte sonst, eine Kette fortzuschreiben, und hätte eine neue begonnen. - **Löschungen werden namentlich genannt.** Ein Backup, das eine verschwundene Datei stillschweigend weglässt, verwehrt genau die Beobachtung, für die man Backups anlegt. - **`deployment/syncova-agent.service` ist auf macOS geschrieben und ungeprüft** — wie `service_windows.go`. `systemd-analyze verify` gibt es hier nicht. **Wichtiger Fund an der Nahtstelle Phase 2/4:** Der Integritätsscan verglich den Hash der *gespeicherten* Form gegen die Kennung — die aber den *Klartext* beschreibt. Bei verschlüsselten Repositories meldete er dadurch **jeden** Chunk als beschädigt. Er nutzt jetzt `ChunkReference.StoredDigest` (`UniqueChunkReferences()` statt `UniqueChunkIdentifiers()`). Ein Prüfwerkzeug, das grundlos Alarm schlägt, wird bald nicht mehr ernst genommen. ### Phase 7 (Proxmox-Provider) — gebaut, Meilenstein auf echter Hardware offen **Die Kette läuft durch: Auftrag → Provider → vzdump → Archiv als Datenstrom → Backup Engine → Repository → zurück auf den Knoten → `qmrestore`.** Ein Test belegt den Rundlauf bitgenau gegen einen Nachbau der API. **Ungeprüft bleibt der entscheidende Schritt: ob die wiederhergestellte Maschine startet** — dafür stand kein Proxmox-Verbund zur Verfügung. Bis dahin ist der Provider nicht freigegeben; der Ablauf für das Produktivsystem steht in `docs/proxmox.md`. - **Die API-Lücke bestimmt die ganze Bauart:** Proxmox kann eine Sicherung anstoßen, aber die entstandene Datei nicht herausgeben — es gibt keinen REST-Endpunkt für Datenträger- oder Archivinhalte, und zum Zurückspielen nimmt die API ausschließlich eine **Volumenkennung** entgegen. Deshalb zwei Nähte: `ArchiveTransport` (lesen) und `ArchiveWriter` (schreiben), je umgesetzt als `LocalArchiveTransport` (Dateizugriff/Freigabe) und `SSHArchiveTransport`. **Bewusst zwei Schnittstellen statt einer:** Ein Weg kann lesend eingerichtet sein, ohne schreiben zu dürfen — wer nur sichert, braucht das Schreibrecht nicht. - **Ohne hinterlegten Wirtsschlüssel keine SSH-Verbindung.** Einen Schalter „Wirtsschlüssel egal" gibt es nicht: Ein Transport, der jeden annimmt, macht aus einem Zwischenangriff eine Einladung — der Angreifer lieferte dann das Archiv, das Syncova für ein Backup hält. - **Fund — `normalizeFingerprint` durfte nicht wiederverwendet werden.** Der bestehende für TLS macht Kleinbuchstaben; bei einem Hexwert richtig, bei dem Base64-Wert von `ssh-keygen -l` **falsch**. Ein kleingeschriebener Fingerabdruck passte auf keinen Schlüssel mehr, und die Verbindung schlüge mit einer Meldung fehl, die nach einem Angriff aussieht. Eigener `normalizeSSHFingerprint`. - **Die Volumenkennung kommt vom Knoten und wird geprüft.** `backup/../../../etc/shadow` läse sonst eine beliebige Datei des **Syncova-Servers** ins Backup. Umgekehrt beim Zurückschreiben: Ein Archivname mit Pfadanteil schriebe die Datei irgendwohin auf den **Proxmox-Knoten**, mit den Rechten des Anmeldekontos. - **Das Archiv wird beim `Close()` des Datenstroms vom Knoten entfernt.** Früher zu löschen zöge dem Leser die Datei unter den Füßen weg; gar nicht zu löschen füllte den Proxmox-Speicher bei jeder Sicherung mit einer zweiten, unverwalteten Kopie, für die keine Aufbewahrungsregel gilt. `KeepArchiveOnNode` ist der ausdrückliche Ausweg. - **Zurückgeschrieben wird unter Zwischennamen, umbenannt erst beim Schließen** (wie beim Ablegen eines Blocks in Phase 2). Ein abgebrochener Transfer hinterlässt kein Archiv, das Proxmox für vollständig hält. Beim SSH-Weg steckt im `Close()` zusätzlich die Auswertung des Rückgabewerts der Gegenseite — voller Speicher ist der häufigste Fall. - **Ein Gast liegt als zwei Objekte im Manifest:** `guest/disk-image.vma` und `guest/configuration.json`. Der Pfad des Abbilds ist **fest** und nicht aus dem vzdump-Dateinamen abgeleitet — der trägt einen Zeitstempel, und ein wechselnder Manifestpfad machte jede Deduplizierung über Backupgrenzen hinweg unmöglich (die Engine vergleicht über den Pfad). - **Die Konfiguration wird vor dem Archiv gelesen.** Danach hieße: nach einem stundenlangen vzdump — und scheiterte sie dann, wäre der ganze Lauf umsonst gewesen. - **„Inkrementell" heißt bei einem Gast das Gegenteil von Phase 6.** Dort ist der Gewinn Zeit; hier liest vzdump jedes Mal die ganze Maschine, und gespart wird ausschließlich **Platz** durch die Deduplizierung. Wer das verwechselt, plant seinen Nachtbetrieb falsch. - **Eine Platte mit `backup=0` macht den Lauf zum Teilfehler.** Nicht weil etwas schiefging, sondern weil die Wiederherstellung sonst eine unvollständige Maschine liefert, die jemand für vollständig hält. - **Der Bestand ist eine Momentaufnahme.** Ein fehlender Gast wird als `missing_since` vermerkt, nie gelöscht: Er könnte abgeschaltet oder verschoben sein, und eine gelöschte Zeile nähme die Zuordnung zu vorhandenen Backups mit — die man genau dann braucht, wenn die Maschine weg ist. Ein zweiter Lauf verschiebt den Zeitpunkt nicht. - **Der Verbundbezug einer Quelle steht als CHECK in der Datenbank.** Bei genau einem Verbund ließe er sich raten — bei zweien sicherte der Lauf die falsche Maschine. - **Zugangsdaten verschlüsselt, `LoadCredentials` getrennt von `GetCluster`.** Wer einen Verbund nur anzeigt, soll die Geheimnisse gar nicht erst im Speicher haben. Real geprüft: Das Token steht nirgends im Klartext in der Zeile. - **`SetArchiveTransport` ist nachträglich, nicht Teil der Optionen.** Der SSH-Weg braucht den Provider selbst, um eine Speicherkennung in einen Pfad aufzulösen — beides im Konstruktor zu verlangen ergäbe eine Henne-Ei-Lage. - **Ein nicht erreichbarer Verbund ist ein 503, kein 500** — und `last_seen_at` wird nur bei Erfolg fortgeschrieben. Bei jedem Versuch zu setzen machte aus „zuletzt erreicht" ein „zuletzt versucht", und ein seit Wochen toter Verbund sähe frisch aus. `unauthorized` ist von `unreachable` getrennt: Die Abhilfe ist eine völlig andere. - **`ReadChangedBlocks` meldet `ErrNotSupported`.** Die QEMU-Schmutzbitmap gibt es nur über das PBS-Protokoll oder QMP. Eine leere Bereichsliste wäre der bequeme Weg und der schlimmste: die Sicherung hielte jede Platte für unverändert. - **Alles ist asynchron.** Ändernde Aufrufe antworten mit einer UPID; wer sie für das Ergebnis hält, meldet einen Snapshot als angelegt, bevor er existiert. Der Statusendpunkt ist **knotenbezogen** (Knoten steckt in der UPID), und **Fehler stehen im Exit-Status, nicht im HTTP-Status**. Bei Fehlschlag wird das Aufgabenprotokoll mit abgerufen — der Exit-Status ist meist nur ein Satz. - **`WaitForTask` hat bewusst keine Zeitgrenze** (vzdump über TB läuft Stunden; eine Grenze bräche genau die großen Maschinen ab). Für Snapshots gilt das Gegenteil: 10 Minuten bedeuten, dass der Gastdienst hängt. - **API-Token statt Ticket** — dauerhaft gültig, einzeln widerrufbar, eigene Rechte. Bei selbstsigniertem Zertifikat **Fingerabdruckbindung statt `InsecureSkipVerify`**: das ist strenger als eine CA-Prüfung, weil nur ein einziges Zertifikat gilt. - **Plattenerkennung, drei Fallen:** `backup=0` nimmt eine Platte aus (wer das übergeht, hält eine unvollständige Maschine für vollständig); `media=cdrom` ist keine Platte; `unused0` ist eine abgehängte Platte **mit** Daten. Dazu: `efidisk0` ist 1 MiB groß und ohne sie startet UEFI nicht, und ein fehlendes `bios`-Feld bedeutet SeaBIOS. - **Die Konsistenzstufe wird nie beschönigt.** `QuiesceGuest` ohne Gastdienst ergibt `crash_consistent` plus Warnung, nicht die verlangte Stufe. - **Restore:** laufender Gast wird nie überschrieben, vorhandener nie ohne Zustimmung, gestartet wird nie unaufgefordert. Die **Plattenzuordnung wird bewusst nicht aus der gesicherten Konfiguration gesetzt** — sie verwiese auf den alten Ort und die Maschine startete nicht; das wird als Warnung ausgewiesen. Die MAC-Adresse dagegen muss erhalten bleiben (Lizenzen, DHCP-Reservierungen). - **`RawConfiguration` wird wortgetreu mitgesichert.** Eine umgedeutete Fassung verlöre die Felder, die wir heute noch nicht kennen. - Nachgewiesen gegen den API-Nachbau: 3 MiB Gastarchiv gesichert (Manifest mit Abbild **und** Konfiguration, ausgenommene Platte als Teilfehler ausgewiesen, Archiv vom Knoten entfernt), Quelle gelöscht, **bitgenau** zurückgeschrieben, MAC-Adresse erhalten, Gast nicht unaufgefordert gestartet. Real gegen den laufenden Dienst: `http://` abgelehnt, SSH ohne Wirtsschlüssel abgelehnt, Viewer 403 auf jeden schreibenden Zugriff, Löschschutz bei verwendetem Verbund (409), Audit-Eintrag geschrieben. - **Offen für das Produktivsystem:** ob die wiederhergestellte Maschine **bootet**; der SSH-Weg (übersetzt und in seiner Fingerabdruckprüfung getestet, aber nie gegen einen echten Knoten gefahren); LXC; eine Oberfläche für Verbünde; Vergleich der Archivgröße gegen die Angabe von Proxmox. ### Phase 8 (Scheduler) — Kette geschlossen **Auftrag → Scheduler → Backup Engine → Repository → Restore läuft durch.** Real nachgewiesen: 38 MiB über die API beauftragt, vom Scheduler gesichert, Integritätsscan sauber, Quelle gelöscht, aus der Zusatzsicherung bitgenau wiederhergestellt. Zweiter Lauf: 0,0 MiB in 0,06 s statt 38,1 MiB in 0,57 s. Es fehlen: Quellen außer Dateisystemen, Aufbewahrung, Prüfung und Benachrichtigung. - **`packages/backupexecutor` setzt `jobs.Executor` um.** Ohne ihn greift `NotImplementedExecutor` mit `EXECUTOR_NOT_CONFIGURED` — stillschweigender Erfolg wäre das gefährlichste Fake-Feature der Anlage: grüne Läufe bei leerem Repository. - **Ein Executor ohne Schlüsselmaterial wird beim Einrichten abgelehnt**, nicht erst beim ersten Lauf um zwei Uhr nachts. `AllowUnencrypted` ist der ausdrückliche Ausweg, mit Warnung bei jedem Start. - **Eine gescheiterte Quelle bricht den Lauf nicht ab**, zählt aber als übergangenes Objekt → Teilfehler. Scheitern alle: `ALL_SOURCES_FAILED`. - **Das Repository wird einmal je Lauf geöffnet**, nicht je Quelle — sonst Wechselspiel um dieselbe Schreibsperre. - **Die Backup-Kennung ist `run--`.** Die Laufkennung hat Vorrang: Aus ihr lässt sich das Backup einem Lauf zuordnen, auch wenn die Datenbank verloren ging. Grenze 64 Zeichen (Kennung wird zum Dateinamen), Quellanteil wird gekürzt. - **`RecordBackup` ist ein Verweis, keine Kopie.** Scheitert er, wird protokolliert statt geworfen: Das Backup liegt sicher im Repository, und ein „gescheitert" löste eine sinnlose Wiederholung aus. - **Je Quelle und Repository genau eine Kette** — zwei Quellen in einer Kette machten die eigenständige Wiederherstellung unmöglich. - **Fund:** Der Begrenzer wurde gebaut und protokolliert, aber beim Bau der Quellanfrage **nie gesetzt** — die Zeile „der lauf ist in der bandbreite begrenzt" erschien, während mit voller Geschwindigkeit gesichert wurde. Ein Feld, das gesetzt aussieht und nie ankommt, ist im Protokoll nicht zu erkennen. `TestExecutorAppliesJobBandwidthLimit` misst deshalb den Durchsatz statt die Konfiguration. - **`jobs.Executor` hält die Schleife frei von Engine, Repository und Providern.** Ohne diese Naht liesse sich das Zusammenspiel von Zeitplan, Fenster, Nebenläufigkeit und Wiederholung nur mit echtem Repository prüfen — also praktisch gar nicht. - **Übergangene Objekte machen einen Lauf zum Teilfehler, auch ohne gemeldeten Fehler.** Die Auswertungsreihenfolge in `evaluateExecution` ist festgelegt: Abbruch → Fehler → übergangene Objekte. Ein Abbruch wiegt am schwersten (ein „gescheitert" löste eine sinnlose Wiederholung aus). - **Ein Teilfehler wird nicht wiederholt.** Die übergangenen Objekte wären beim nächsten Versuch dieselben; er verlangt einen Blick, keine Wiederholung. - **Versäumte Läufe werden übersprungen, nicht nachgeholt.** Drei Tage Ausfall ergeben *einen* Lauf, nicht drei — die Daten von vorgestern gibt es nicht mehr. Die Zahl wird protokolliert. - **Kein Drift:** Der nächste Zeitpunkt wird vom *geplanten* aus gerechnet, nicht vom tatsächlichen Beginn — und **beim Übernehmen** fortgeschrieben, nicht nach dem Lauf (sonst bliebe der Auftrag bei langem Backup fällig). - **Lebendmeldung alle 30 s, Freigabe nach 5 min.** Stirbt ein Server mitten im Lauf, blockiert dessen `running`-Zeile den Auftrag über den Teilindex **dauerhaft** — ohne dass jemand einen Fehler sähe. Die Frist muss deutlich über dem Meldeabstand liegen, sonst liefe derselbe Auftrag zweimal; `LoopOptions.Validate()` lehnt das ab. - **Ein verwaister Lauf wird als gescheitert vermerkt, nicht gelöscht** (`SCHEDULER_LOST`, Klasse `transient`, damit die Wiederholung greift). - **SIGTERM bricht laufende Vorgänge ab**, statt sie zu Ende zu führen — sonst hinge der Neustart am längsten Backup. Das Ergebnis wird mit eigenem Kontext geschrieben, sonst bliebe die Zeile auf `running`. - **`POST /jobs/{id}/run` antwortet 202, nicht 201:** Der Lauf ist eingereiht, die Sicherung hat nicht begonnen. Zweiter Anstoß bei laufendem Auftrag → **409**, nicht 500. - **Fund:** `bytes_processed = $3` und `throughput_bps = ($3 / EXTRACT(...))` im selben UPDATE ließen PostgreSQL zwei Typen für denselben Parameter ableiten (`42P08`). Ohne den `::bigint`-Cast wäre **jeder** Lauf auf `running` hängen geblieben — der Fehler wurde nur geloggt, nicht geworfen. - **Die Zeitumstellung ist der Punkt, an dem Scheduler stillschweigend danebenliegen.** Frühjahr: „täglich 02:30" existiert am Umstellungstag nicht — es wird der nächste gültige Zeitpunkt genommen, statt den Lauf ausfallen zu lassen. Herbst: 02:30 gibt es zweimal — der Auftrag läuft **einmal**, sonst entstünden zwei Ketten. Gegen die echten Termine 2026 in `Europe/Berlin` geprüft. - **Kalendersuche mit `AddDate`, nie mit `Add(24h)`.** An Umstellungstagen hat ein Tag 23 oder 25 Stunden. - **Ohne Zeitzone gilt UTC, nicht die Serverortszeit.** Sonst liefe dieselbe Konfiguration auf zwei Servern zu verschiedenen Zeiten. - **Cron-Sonderregel:** Sind Tag *und* Wochentag eingeschränkt, gilt **ODER**. `0 0 13 * 5` heißt „am 13. oder freitags". Als UND umgesetzt liefe der Plan fast nie — und es fiele erst nach Monaten auf. - **Ein durch ein Wartungsfenster verhinderter Lauf wird verschoben, nicht übergangen.** Dass ein Lauf verspätet ist, sieht man; dass er fehlt, nicht. - **Ein Erlaubnisfenster für Auftrag A sperrt Auftrag B nicht** — sonst fielen alle Sicherungen aus, sobald irgendwo eines existiert. - **Verhungerungsschutz mit Deckel:** 50 Punkte je Wartestunde, höchstens 100. Ohne Alterung verhungert ein niedrig eingestufter Auftrag für immer; ohne Deckel überholte ein alter jede kritische Sicherung. - **Ein Teilfehler erfüllt keine Abhängigkeit.** Wer eine Datenbank sichert und danach das Anwendungsverzeichnis, will nicht das Verzeichnis zu einer halben Datenbank. - **Wiederholt wird nur bei transient/network/repository/source.** Ein Anmeldefehler behebt sich nicht durch Warten; ein Integritätsfehler wird nur später bemerkt. Unbekannte Fehler gelten als **dauerhaft** — der umgekehrte Standard verdeckte die Ursache. - **Backoff-Streuung wirkt nur nach unten**, sonst wäre der Deckel keiner. Gerechnet in Gleitkomma: Ein Schieben um >62 Stellen machte aus langer Wartezeit eine negative. - **Ein Bandbreitenbegrenzer je Lauf**, geteilt über alle Quellen und Arbeiter — sonst ein Vielfaches der Rate. Token-Bucket statt Zeitfenster (das erlaubt an der Grenze die doppelte Rate). `100Mbit` ≠ `100MB`. - **Begrenzt wird das Lesen von der Quelle**, nicht das Schreiben ins Repository. Wegen des Gegendrucks der Pipeline bindet dieser eine Punkt die gesamte Last. Gemessen: 19,1 MiB in 0,37 s ohne Grenze gegen 18,17 s bei 1 MiB/s. - **Der Teilindex `backup_job_runs_single_active_idx` + `FOR UPDATE SKIP LOCKED`** verhindern, dass zwei Control-Server denselben Auftrag starten. Mit vier gleichzeitigen Servern geprüft. - **Regeln als CHECK in der Datenbank**, etwa `files_skipped = 0 OR status <> 'succeeded'`. Im Code müsste jede Stelle sie einhalten — eine vergisst es. Alle acht gegen die echte DB geprüft. - **Ein RPO, den der Zeitplan nicht einhalten kann, wird beim Anlegen abgelehnt.** Der Betreiber glaubte sonst, vier Stunden zu verlieren, während täglich gesichert wird. - **`syncova-migrate force `** rettet nach abgebrochener Migration. Es führt **kein SQL aus**, sondern behauptet einen Stand — daher Bestätigung über `SYNCOVA_MIGRATE_CONFIRM_FORCE` und Ablehnung auf sauberer Datenbank. ### Phase 9 (Recovery Engine) **Die Vorabprüfung ist der Kern, nicht das Zurückschreiben.** `POST /restores/validate` schreibt nichts und stellt fest, ob eine Wiederherstellung gelingen *kann* — insbesondere, ob **jeder benötigte Block noch da ist**. Ein Manifest allein belegt nur, dass jemand einmal etwas gesichert hat. - **Die Prüfung läuft zweimal:** beim Anlegen und erneut unmittelbar vor dem Schreiben. Der gespeicherte Bericht kann Tage alt sein; in der Zwischenzeit kann ein Block verschwinden. - **Der Befund nennt die betroffene Datei**, nicht nur eine Zahl. Übersprungene Blockprüfung erscheint als Hinweis im Bericht — sonst hielte man einen halben Nachweis für einen ganzen. - **Drei Hürden vor dem Überschreiben:** Kennzeichen `overwrite_existing`, eigene Berechtigung `restores.overwrite` (nicht in `restores.execute` enthalten) und `confirm_overwrite`, das den **Zielpfad wörtlich wiederholt**. Ein versehentlich gesetztes Kennzeichen in einem Skript reicht damit nicht aus. Läuft der Schalter ins Leere, entfällt die Bestätigung — ein Ritual ohne Anlass gewöhnt das Wegklicken an. - **Der Prüfpunkt hält einen einzigen Pfad**, weil das Manifest fest sortiert ist. Er entsteht erst, **nachdem** die Datei vollständig und umbenannt am Platz liegt — ein Prüfpunkt auf eine halbe Datei wäre schlimmer als keiner. Geschrieben höchstens alle paar Sekunden, sonst eine Million Datenbankschreibvorgänge bei einer Million Dateien. - **Bei einer Fortsetzung greift der Schutz gegen ein volles Ziel nicht** — dort liegt der bereits geschriebene Teil. Ihn über `overwrite_existing` auszuhebeln erlaubte zugleich das Überschreiben fremder Daten. - **Kein automatischer Wiederholungsversuch.** Ein zweiter Lauf in ein halb gefülltes Ziel kann Daten beschädigen, die der erste bereits am Platz hatte. Auch verwaiste Aufträge werden nicht erneut eingereiht; die Sitzung bleibt offen. - **Eine Wiederherstellung zur Zeit** (Standard) und ein Teilindex gegen zwei gleichzeitige in **dasselbe Ziel** — sie schrieben sich gegenseitig zu, und beide endeten erfolgreich. - **Das Repository wird schreibgeschützt geöffnet:** Eine Wiederherstellung liest nur und muss nicht auf die Schreibsperre einer laufenden Sicherung warten. - **Die Vorabprüfung braucht nur `restores.read`** — wer den Zustand der Backups beurteilen soll, muss sie ausführen können. ### Phase 10 (Verification und Recovery Assurance) **Nur der Wiederherstellungstest ist ein Nachweis.** Manifest-, Block- und Kettenprüfung sind Indizien — gute, aber Indizien. Deshalb wiegt er in der Bewertung am schwersten (25 von 100) und hängt an einem eigenen Recht `verification.restore_test`: Er liest das gesamte Backup und schreibt es versuchsweise zurück. - **Die Blockprüfung vergleicht gegen `ChunkReference.StoredDigest`,** nicht gegen die Chunk-Kennung — die beschreibt bei einem verschlüsselten Repository den Klartext. Derselbe Fehler wie beim Integritätsscan der Phase 2. Der Nebeneffekt ist der eigentliche Gewinn: Eine Integritätsprüfung läuft dadurch **ohne Datenschlüssel**. - **Ein Befund löscht `last_verified_at` und `last_restore_test_at`.** Ohne diesen Schritt stand ein Backup nach Behebung des Schadens sofort wieder als `recoverable` da — auf Grundlage eines Tests, der **vor** dem Schaden lief. Im Nachweis aufgefallen. - **Beschädigt heißt 0 %.** Ein Backup mit einem einzigen beschädigten Block kam auf 70 %, weil Aktualität, Verschlüsselung und ein früherer Test weiterhin zählten. „70 %" liest sich wie „weitgehend in Ordnung" — es ist aber nicht zu 70 % wiederherstellbar, sondern gar nicht. Ebenfalls im Nachweis aufgefallen; Regressionstest vorhanden und als fangend geprüft. - **Unbekannt zählt nie als gut.** Jede Eingangsgröße trägt `is_known`; `missing_measurements` ist die Handlungsanweisung. `HasOffsiteCopy` ist fest `false`, weil es die Funktion nicht gibt — wohlwollend zu schätzen wäre die bequeme und falsche Entscheidung. - **Zwei CHECK-Constraints stützen die Einstufung:** `recoverable` verlangt `last_restore_test_at`, `verified` verlangt `last_verified_at`. Die stärkste Aussage der Anlage lässt sich damit auch durch einen künftigen Codefehler nicht ohne Nachweis vergeben. - **Eine bestandene Blockprüfung hebt auf `verified`, stuft aber nie von `recoverable` herab.** Ein auf einen Teilbaum beschränkter Restore-Test hebt gar nicht — er prüfte einen Teil, nicht das Backup. - **Ein Fehlschlag der Prüfung ist kein Befund am Backup.** Repository nicht erreichbar → Auftrag `failed`, Einstufung unberührt. Ein Prüfwerkzeug, das grundlos Alarm schlägt, wird bald nicht mehr ernst genommen. - **Die Bewertung wird bei jedem Aufruf neu berechnet,** nicht gelesen: Sie hängt am Alter der Messungen und veraltet von selbst. - **Teilindex gegen zwei gleichzeitige Prüfungen desselben Backups** (409). Ohne Freigabe verwaister Prüfungen sperrte er das Backup dauerhaft. - Vollständiger Lebenszyklus gegen den laufenden Dienst nachgewiesen: ungeprüft 35 % → geprüft 55 % → wiederherstellbar 90 % → ein Byte gekippt 0 % `corrupted` (Befund nennt die Datei) → repariert 55 % → Test wiederholt 90 %. Siehe `docs/verification.md`. ### Phase 11 (Immutability und Aufbewahrung) **Die Durchsetzungsstufe wird gemessen, nicht behauptet.** `MeasureEnforcement` legt Probedateien an und versucht sie zu löschen; gemeldet wird nur, was das Betriebssystem nachweislich verhindert. `storage` (S3 Object Lock/WORM) existiert als Begriff und wird nie vergeben — es ist nicht umgesetzt. - **`0400` verhindert kein Löschen.** Unter POSIX hängt das Entfernen am Schreibrecht des *Verzeichnisses*. Der ursprüngliche gehärtete Modus nannte das Löschschutz; real gemessen und als Regressionstest festgehalten. Den Schutz leistet das Unveränderlich-Kennzeichen (`UF_IMMUTABLE` via chflags, `FS_IMMUTABLE_FL` via ioctl). - **Fund im Angriffsversuch (zweimal erfolgreich):** Ein geschütztes Manifest überlebte `rm -rf`, seine **Chunks nicht** — zurück blieb ein Backup, das sich für vollständig ausgibt und leer ist. Beim zweiten Versuch fehlten **Descriptor und Datenschlüssel**: Daten da, nicht deutbar, nicht entschlüsselbar. Geschützt sind jetzt Manifeste, Chunks, Schutzvermerke, Descriptor und Datenschlüssel — der Katalog bewusst nicht (nur Beschleuniger). - **`Sys()` liefert `*syscall.Stat_t`, nicht `*unix.Stat_t`.** Die Typzusicherung auf den falschen schlug still fehl; das Kennzeichen wurde nie gesetzt, und die Messung meldete folgerichtig `advisory`. Nur weil sie misst statt behauptet, fiel es auf. - **Ein gehärtetes Repository lässt sich nicht mit `rm -rf` entfernen** — auch nicht von `t.TempDir()`. Tests müssen den Schutz selbst freigeben. Eine rekursive Aufhebungsfunktion gibt es bewusst nicht. - **Der Schutzvermerk liegt neben dem Manifest** (`.hold.json`), nicht darin: Das Manifest ist über `ContentHash` versiegelt. Ein **unlesbarer** Vermerk gilt als Schutz — die umgekehrte Auslegung machte aus einer kaputten Datei einen Datenverlust. - **Verlängern ja, verkürzen nie** — auch nicht für Administratoren. Ein Legal Hold verlangt eine Begründung; ohne sie traut sich später niemand, ihn aufzuheben. - **`keep_last` schützt das letzte vorhandene Backup.** Ohne es löschte „7 Tage" bei einem drei Wochen nicht gesicherten System *jedes* Backup. Eine Regel ohne jede Haltevorgabe wird abgelehnt; eine ergänzte 1 wird in der Antwort ausgesprochen statt still eingesetzt. - **Fund:** Der Schutz von Elternbackups hätte jede Aufbewahrung verhindert — in einer fortlaufenden Kette ist jedes Backup außer dem jüngsten ein Elternteil. Da Syncova-Manifeste vollständig sind (Phase 6), steht die Zusicherung jetzt im Manifest (`self_contained_restore`); ein fehlendes Feld bedeutet „unbekannt" und schützt weiter. Der Test löscht das Elternbackup und liest danach jeden Block des Kindes. - **`backups.delete` und `immutability.manage` sind getrennt.** Wer aufräumen darf, darf keinen Schutz aufheben — das ist der Schritt, der einem Angreifer den Weg öffnet. - **„Gelöscht, aber nichts frei" ist kein Fehler**, sondern Deduplizierung. Die Zusammenfassung sagt es, sonst erzeugt jede solche Ausgabe eine Rückfrage. - Nachgewiesen: `rm -rf` gegen ein gehärtetes Repository → 15 verweigerte Löschungen, Backup danach vollständig. Siehe `docs/immutability.md`. ### Phase 12 (Weboberfläche) **Der Umgang mit unfertigen Bereichen ist die Entscheidung dieser Phase.** Alle fünfzehn Seiten aus §14 erscheinen im Menü; unfertige tragen den Vermerk „noch nicht verfügbar" und führen auf eine Seite, die sagt, *was* fehlt und *wo dieselbe Auskunft heute steht*. Nur die fertigen zu zeigen verschwiege den Ausbaustand, leere Masken täuschten ihn vor. - **Sieben von zehn Kennzahlen haben eine Datengrundlage.** Kritische Meldungen, Kapazitätsprognose und Security Score erscheinen mit Begründung statt mit einer Null — „0 kritische Meldungen" hieße „keine Probleme" und bedeutete „es wird nicht geprüft". - **Keine Läufe sind nicht 100 %.** Ohne Lauf in sieben Tagen gibt es keine Erfolgsquote; die Kennzahl meldet `warning`. Ein Dashboard, das bei ausgefallener Sicherung grün zeigt, ist schlimmer als keines. Ein Teilfehler zählt nicht als Erfolg. - **Nicht bezifferbar ist nicht null, und 0,0004 % ist nicht 0 %.** Ohne hinterlegte Kapazität gibt es keinen Prozentsatz; kleine Werte erscheinen als `< 0,1 %`, weil „null Prozent" wie „nichts abgelegt" liest. - **Fund:** `legal_hold = false OR immutable_until > now()` liefert in der SQL-Dreiwertlogik **NULL**, sobald keine Frist gesetzt ist — nicht `false`. Ein NULL lässt sich nicht in ein `bool` lesen, und die gesamte Liste der Wiederherstellungspunkte schlug fehl. Die Regel steht jetzt als `protectionExpression` an genau einer Stelle. - **Navigation über die History-API, keine Router-Bibliothek** (~50 Zeilen). **Betriebsfolge:** Das Bundle braucht einen SPA-Fallback (`try_files $uri /index.html`), sonst ergibt ein Neuladen auf `/recovery-points` einen 404. - **Der Ladezustand wird abgeleitet, nicht im Effekt gesetzt.** Das Ergebnis trägt den Schlüssel seiner Anfrage; passt er nicht zum aktuellen, läuft sie noch. Ein `setState` im Effektkörper löste eine zweite Renderrunde aus (derselbe Lint-Fehler wie in Phase 8). Nebeneffekt: Beim Filterwechsel blitzt die Tabelle nicht auf. - **Berechtigungen im Menü sind Anzeige, keine Sicherung.** Sie verhindern Sackgassen; geprüft wird auf dem Server. - **Jede Fehleranzeige nennt `request_id`** — ohne sie bleibt „es hat nicht funktioniert". - Siehe `docs/web-ui.md`. ### Phase 13 (Kennzahlen und Diagramme) **Eine Lücke ist keine Null.** Ein Zeitfenster ohne Sicherungslauf hat *keinen* Durchsatz — nicht null Byte je Sekunde. Jeder Punkt trägt `has_value`; das selbst geschriebene SVG-Diagramm unterbricht die Linie, statt sie durch den Nullpunkt zu ziehen. Genau das beherrschen die gängigen Diagrammbibliotheken standardmäßig falsch. - **Zehn von zwölf Reihen werden aus vorhandenen Tabellen aggregiert**, nicht doppelt gespeichert: Zwei Quellen für dieselbe Aussage laufen auseinander, und man merkt es erst, wenn jemand nachrechnet. `metric_samples` nimmt nur auf, was sonst verloren geht — Momentaufnahmen wie `used_bytes`, die überschrieben werden. - **Fund:** `PostgreSQL wertet NaN = NaN als wahr`, anders als IEEE 754. Der übliche CHECK `value = value` ließ NaN durch; ein einziger NaN verseucht jede Summe der Reihe lautlos. Jetzt `value <> 'NaN'::float8`. - **Fund:** `json:"value,omitempty"` ließ einen **gemessenen Wert von null** aus der Antwort verschwinden — `has_value: true` ohne `value`. Ein Feld, das je nach Wert da ist oder nicht, ist die unangenehmste Sorte Schnittstelle: Sie funktioniert fast immer. - **Fund:** Ein Jahr geteilt durch sieben Tage ergibt 52,14 Fenster; abgerundet fielen die letzten ein bis sieben Tage heraus. Der Jahresverlauf zeigte **null Messungen**, obwohl am selben Tag Läufe stattfanden. `BucketCount()` rundet jetzt auf. Der Regressionstest griff zunächst nicht, weil er den Fensterbeginn mit `time.Truncate` (ab Unix-Epoche) statt ab Fensteranfang rechnete — wie die SQL-Abfrage. - **Fund:** Das Kompressionsdiagramm war als verfügbar geführt und konnte nie Daten haben — `compressed_bytes` wird von keiner Stelle beschrieben. Jetzt als nicht verfügbar ausgewiesen. `encrypted_bytes` einzusetzen wäre falsch: Der Verschlüsselungsaufwand erschiene als schlechte Kompression. - **Fund (aus Phase 12):** Das Repository-Widget prüfte auf die Zustände `offline` und `archived`, die das Schema nie kannte (nur `active`, `read_only`, `unavailable`, `maintenance`). Es meldete „Alle 5 Repositories sind erreichbar" bei vier nicht erreichbaren. - **Der Durchsatz wird neu berechnet**, nicht aus `throughput_bps` gelesen: Die Spalte bleibt bei kurzen Läufen leer. Läufe unter einer Sekunde fallen heraus — dort bestimmt die Messungenauigkeit das Ergebnis. - **Die Werteachse beginnt immer bei null.** Eine abgeschnittene Achse lässt kleine Schwankungen wie Einbrüche aussehen — der häufigste Weg, mit korrekten Zahlen etwas Falsches zu zeigen. - **Speicherwachstum misst das Dateisystem, nicht das Repository** (statfs statt Durchlauf über Millionen Blöcke). Bei geteilter Ablage wächst die Kurve auch durch fremde Daten — das steht in der Beschreibung. - Siehe `docs/metrics.md`. ### Phase 14 (Meldungen und Benachrichtigungen) **Der Feind ist nicht der fehlende Alarm, sondern der Alarm, den niemand mehr liest.** Ein System, das jede Minute dieselbe Meldung erzeugt, wird nach drei Tagen weggeklickt — und dann fehlt die eine, auf die es ankam. Zwei Eigenschaften tragen alles: - **Eine Ursache, eine Meldung.** Der Fingerabdruck entsteht aus Regel und Gegenstand, **nicht** aus dem Zeitpunkt; ein Teilindex `WHERE status IN ('open','acknowledged')` macht die Doppelung unmöglich. Die Regel steht in der Datenbank, nicht in der Anwendung: Zwischen Nachsehen und Schreiben passt ein zweiter Control-Server. Der wiederholte Befund erhöht einen Zähler — einmal ist ein Zwischenfall, zwanzigmal ein Zustand. - **Meldungen lösen sich selbst auf.** Jede Regel liefert die **derzeit** zutreffenden Befunde; was fehlt, wird automatisch geschlossen. Ohne diesen Schritt steht nach zwei Wochen eine Liste erledigter Probleme da, und die aktuelle geht darin unter. Der Auswerter beschreibt den Ist-Zustand, statt Ereignisse zu zählen. - **Bestätigen heißt nicht Erledigen.** Eine bestätigte Meldung bleibt offen und in der Liste; sonst verschwände der Zustand aus der Übersicht, obwohl er weiterbesteht. - **`x = ANY(NULL)` ist niemals wahr.** Eine leere Befundliste — der Normalfall, sobald alles in Ordnung ist — muss als leeres Array übergeben werden, sonst löst sich nichts auf und die Meldung bleibt für immer stehen. - **Die Ransomware-Regel heißt „Verdacht", nicht „erkannt".** Bricht die Deduplizierung ein, sieht das nach massenhafter Verschlüsselung aus — oder nach einem großen Update. Eine Anlage, die „Ransomware erkannt" meldet und danebenliegt, wird beim nächsten Mal ignoriert. - **Die Zertifikatsregel wird nicht ausgewertet:** `agent_certificates` wird von keiner Stelle beschrieben, die Agenten weisen sich über Betriebstokens aus. Eine Regel, die dauerhaft schweigt, ist gefährlicher als keine — sie erweckt den Eindruck, es werde geprüft. - **Nur neue Meldungen werden zugestellt**, aktualisierte nicht. Jeder Kanal hat eine Schwelle (Standard `high`): Ohne sie schaltet der Bereitschaftsdienst nach einer Woche die Benachrichtigungen ab, und dann kommt auch die kritische nicht mehr an. - **Webhook verlangt HTTPS** (`allow_insecure` als ausdrücklicher Ausweg); einen Schalter „Zertifikat egal" gibt es nicht. Zustellung real gegen echten SMTP- und HTTP-Server geprüft, nicht gegen Attrappen. - **Kanäle hängen am Einstellungsrecht, nicht am Meldungsrecht:** Wer Benachrichtigungen umleitet, kann erreichen, dass niemand mehr von einem Ausfall erfährt. Anlegen und Löschen werden auditiert. - Das Dashboard-Widget „Kritische Meldungen" und die Seite „Meldungen" sind damit **verfügbar** — beide standen seit Phase 12 als benannte Lücke. Siehe `docs/alerting.md`. ### Phase 15 (Security Center) **Jeder Befund trägt Schweregrad, Erklärung, betroffenes Objekt und Empfehlung** — der Plan (§17) verlangt genau das, und `Finding.Validate()` erzwingt es: Ein unvollständiger Befund wird abgelehnt, statt ausgeliefert zu werden. Ein Befund ohne Empfehlung ist eine Beunruhigung; er sagt, dass etwas nicht stimmt, und lässt den Betreiber damit allein. - **Zehn Bereiche, zwei davon nicht prüfbar:** Kopie an einem zweiten Ort (Backup Copy nicht umgesetzt) und Zertifikate der Agenten (`agent_certificates` wird von keiner Stelle beschrieben — dieselbe Lage wie bei der Meldungsregel aus Phase 14). - **Ein ungeprüfter Bereich geht nicht in die Rechnung ein** — weder positiv noch negativ. Als bestanden zu werten wäre Schönfärberei, als durchgefallen eine Behauptung. Neben der Prozentzahl steht deshalb immer `maximum_score` und die Liste der ungeprüften Bereiche; ab drei sagt die Zusammenfassung „Die Zahl ist eine Vermutung, keine Aussage." - **Ein kritischer Befund deckelt die Einstufung auf `unzureichend`** — unabhängig von der Prozentzahl. Real geprüft: 88 von 100 Punkten mit einem kritischen Befund ergeben trotzdem `unzureichend`. Dieselbe Regel wie beim beschädigten Backup in Phase 10. - **Das höchste Gewicht hat der Löschschutz (20).** Ohne ihn genügt ein kompromittiertes Konto, um alles zu vernichten; Verschlüsselung und MFA halten dann niemanden auf. Die Gewichte folgen dem Grundsatz: Was den Verlust verhindert, wiegt schwerer als was ihn erschwert. - **Der Score wird bei jedem Aufruf neu berechnet**, gespeichert wird nur sein **Verlauf** als Reihe `security_score` in `metric_samples` (Phase 13). Das Dashboard-Widget liest die letzte Messung statt neu zu rechnen — sonst hinge die Übersicht an zehn Abfragen und `jobs` bekäme eine Abhängigkeit auf `security`. Das Alter der Zahl steht dabei. - Nachgewiesener Zyklus: 35 % `unzureichend` (2 kritisch) → Repository gehärtet und gemessen → 40 % (1 kritisch) → MFA eingerichtet → 57 % `verbesserungsbedürftig`, 0 kritisch, belastbar. Die Einstufung wechselt genau beim letzten kritischen Befund, nicht bei einer runden Zahl. - Damit ist auch das letzte Dashboard-Widget aus Phase 12 verfügbar: **9 von 10 Kennzahlen** haben eine Datengrundlage. Siehe `docs/security-center.md`. ### Phase 16 (Ransomware-Heuristik) **Die Vorgabe steht in vier Worten: „Do not make destructive decisions automatically. Alert first."** Das Paket löscht nichts, sperrt nichts, hält nichts an. Der Grund ist nicht Vorsicht, sondern Erfahrung: Eine Heuristik, die selbsttätig handelt, macht aus jedem Fehlalarm einen Schaden — und ein Betriebssystem-Update sieht von außen aus wie ein Verschlüsselungsangriff. - **Der Entropie-Indikator kostet nichts, weil die Engine ihn ohnehin bildet.** Ob ein Block sich komprimieren ließ, entscheidet `ChunkTransformer.Transform` bei jedem Block; bislang verschwand die Erkenntnis im Marker-Byte, jetzt gibt `Transform` sie zusätzlich zurück. Eine eigene Entropiemessung wäre derselbe Rechenaufwand ein zweites Mal. **Gezählt werden nur neue Blöcke** — ein deduplizierter wurde früher schon bewertet und verwässerte den Anteil genau im entscheidenden Lauf. - **Median und mittlere absolute Abweichung, nicht Mittelwert und Standardabweichung.** Ein einziger Ausreißer ist genau der Fall, den wir erkennen wollen; würde er den Basiswert mitbestimmen, höbe er die Schwelle an, gegen die er gemessen wird. Bei fünf normalen Läufen (~100) und einem Angriff (50 000) läge der Mittelwert über 8000 — der nächste Angriff derselben Größe fiele nicht mehr auf. - **Unter fünf Vergleichsläufen lautet die Einstufung `unknown`, ausdrücklich getrennt von `none`.** Wer nicht messen kann, hat nichts gemessen. Eine geratene Schwelle wäre schlechter als keine. - **Backups aus der Zeit vor der Phase fließen nicht als Nullwerte ein.** Das zöge jeden Basiswert nach unten und ließe jeden neuen Lauf auffällig erscheinen; sie werden übersprungen. - **Zwei auffällige Signale für `high`, nicht eines.** Ein einzelnes hat viele harmlose Ursachen; zwei zugleich sind das Muster massenhafter Verschlüsselung — viele geänderte Dateien **und** kaum noch komprimierbare Daten. - **Fund:** Ein Test meldete „drei statt zwei Dateien" als Befund (50 % Anstieg bei Streuung null). Daher hat jedes Signal eine eigene **absolute Untergrenze**: 20 Objekte, 100 MiB, 10 Prozentpunkte. Zwanzig Dateien mehr sind ein Ereignis, zwanzig Bytes mehr sind Rauschen — eine gemeinsame Grenze gibt es nicht. - **Die Erklärung nennt die Untergrenze, statt sie zu verschweigen.** „Der Wert stieg von 336 auf 3 004 350, das liegt unter der Größenordnung" liest sich wie ein Fehler des Werkzeugs — und wer dem Werkzeug einmal misstraut, liest auch den echten Befund nicht mehr. - **Die Endungsverteilung ist auf 20 Einträge gedeckelt** (Sammelposten `(weitere)`, vom Signal ausgenommen). Ohne Deckel bliese ausgerechnet der Angriff mit seinen Zufallsendungen die JSONB-Spalte auf. - **Nachweis in beide Richtungen**, weil ein Detektor, der auch jeden großen Arbeitstag meldet, nach einer Woche ignoriert wird: 200 neue komprimierbare Dokumente → `elevated`, 1 von 6 Signalen. 150 Dateien durch Zufallsdaten mit Endung `.locked` ersetzt und die Originale gelöscht → `HIGH`, 4 von 6, Anteil unkomprimierbarer Blöcke 0 % → 100 %. Siehe `docs/ransomware.md`. - **Bekannte Grenzen:** Bei Quellen unter 100 MiB tragen die beiden Byte-Signale nichts bei. Ein über Wochen schleichender Angriff verschiebt den Basiswert mit sich — dagegen hilft nur die Unveränderlichkeit aus Phase 11. ### Phase 17 (Berichte) **Ein Bericht ist das Dokument, das die Anlage verlässt.** Er landet in einer Tabellenkalkulation und in einem Prüfordner; was darin steht, wird Monate später ohne Rückfragemöglichkeit gelesen. Eine erfundene Null überlebt dort jede mündliche Erläuterung — deshalb trägt jede Kennzahl `is_known` und im CSV bleibt die Wertspalte leer mit Begründung in der Anmerkung. Eine leere Zelle lässt sich nicht versehentlich summieren; eine Null wird summiert, gemittelt und gezeichnet. - **Das Modell ist formatunabhängig.** CSV, JSON und PDF sind drei Sichten auf dieselbe Struktur; kein Bericht weiß, in welchem Format er ausgegeben wird. Sonst müsste jeder der neun dreimal geschrieben werden — und beim zehnten vergisst jemand eines. Tabellenzellen sind bereits Text (sonst formatierten CSV und PDF dieselbe Zahl verschieden), **Kennzahlen dagegen Rohwerte** (eine Tabellenkalkulation soll rechnen können). - **RPO lässt sich messen, RTO nicht.** In der RTO-Spalte steht eine Messung aus einer tatsächlich durchgeführten Wiederherstellung oder „nicht gemessen" — nie eine Hochrechnung aus Datenmenge und Durchsatz. Das wäre die bequemste Zahl des Berichts und die einzige, auf die sich im Ernstfall niemand verlassen könnte. Eine RTO-Vorgabe ohne Messung ist **unbekannt**, nicht erfüllt und nicht verletzt. - **Ein Zustandsbericht bekommt keinen Zeitraum.** Die Belegung wird nicht historisiert; „vom letzten Dienstag" kann es nicht geben. Ein angefragter Zeitraum wird ignoriert — ihn anzunehmen und nicht auszuwerten wäre die freundlichste Art zu lügen. - **Der Bericht für Prüfungen liefert Messwerte, keine Urteile** (PROMPT §73: keine Zertifizierung vortäuschen). Kein „erfüllt", kein Haken, keine Norm. - **PDF ist selbst geschrieben** (Standardschriften, WinAnsi, eigene Helvetica-Breitentabelle). „Where practical" für nicht praktikabel zu erklären wäre bequem — ein Prüfbericht wird als PDF verlangt, nicht als CSV. Die Querverweistabelle ist der einzige Teil, dessen Fehler erst beim Empfänger auffällt; ein Test prüft jeden Verweis auf einen echten Objektbeginn. - **Fund in der Sichtprüfung:** Ein gleichmäßiger Stauchfaktor kürzte „TEILWEISE FEHLGESCHLAGEN" zu „TEILWEISE FEHL…", weil daneben ein langer Pfad stand. Jetzt gibt nur die breiteste Spalte ab (gemeinsame Obergrenze statt Skalierung). - **Fund im Nachweis:** „Längste Laufzeit: 0" stand als Messung da, während die mittlere korrekt unbestimmbar war — `COALESCE` liefert eine Null, und im Bericht sah sie aus wie ein Lauf in null Sekunden. - **Fund vor dem Nachweis:** Die Abfrage nutzte `users.is_active` — eine Spalte, die es nie gab (das Schema führt `status`). Der Bericht wäre bei jedem Abruf gescheitert. - **Jeder Abruf wird auditiert** (`REPORT_GENERATED`). Ein Bericht liest nur, ist aber ein **Datenexport**: Sicherheits- und Prüfbericht nennen die Schwachstellen in geordneter Form. Scheitert das Protokollieren, wird trotzdem ausgeliefert — anders als bei destruktiven Handlungen. - **Dateien gehen über `downloadApiFile`, nicht `requestApi`.** Eine Datei trägt keine Antworthülle; der Fehlerfall dagegen schon — deshalb Inhaltstyp prüfen, sonst landet eine Fehlermeldung als „bericht.pdf" im Download-Ordner. - Nachgewiesen: 27 von 27 Kombinationen erzeugt, PDF von CoreGraphics gerendert und angesehen, echte Wiederherstellung ausgeführt (RTO danach mit 36 ms belegt), Zeitraum ohne Läufe erzeugt keine 100-Prozent-Quote. Siehe `docs/reports.md`. ### Phase 18 (Disaster Recovery) **Eine Konfigurationssicherung, die nur auf dem Control-Server liegt, ist beim Verlust des Control-Servers wertlos.** Server und Datenbank gehen typischerweise gemeinsam verloren; der einzige Ort, der das überlebt, ist das Repository. `syncova-dr export` schreibt die Control-Plane-Konfiguration nach `metadata/control-plane/`. - **Der Sicherungssatz enthält kein einziges Geheimnis** — keine Passwort-Hashes, keine TOTP-Geheimnisse, keine Zugangsdaten. Bei den Benachrichtigungswegen fällt auch die **Konfiguration** heraus: Dort steht die Webhook-Adresse, und die trägt oft das Token im Pfad. Ein Feld einzeln zu schwärzen hieße, bei jedem neuen Kanaltyp erneut daran zu denken. Was bleibt, ist ein Merkzettel. Ein Test prüft die serialisierte Form, nicht die Struktur. - **Die Liste der Auslassungen steht im Satz selbst.** Wer eine Anlage aus dem Nichts wiederherstellt, hat die Betriebsanleitung nicht dabei. - **Nichts läuft von selbst wieder an:** Aufträge angehalten, Repositories „nicht erreichbar", Konten deaktiviert, Kanäle abgeschaltet, `last_verified_at` leer. Ein Zeitplan, der nachts von selbst anläuft, könnte auf ein halb wiederhergestelltes System schreiben. - **Das Einspielen ist eine Transaktion.** Alles oder nichts — eine halb wiederhergestellte Anlage sieht arbeitsfähig aus und scheitert beim ersten Lauf. Im Nachweis real eingetreten; die Datenbank blieb leer. - **`inspect` braucht keine Datenbank.** Nach einem Totalverlust will man zuerst sehen, was da ist. - **Kennungen entstehen deterministisch (UUIDv5).** Eine zweite Übernahme desselben Repositorys ergibt dieselben Kennungen; mit Zufallswerten entstünden Dubletten. - **Fund — die Manifest-Statistik war strukturell null.** Die Engine schrieb über `WriteTransformedChunk` unmittelbar am Repository und damit an der Zählung der Schreibsession vorbei; jedes Manifest trug `logical_bytes: 0`. Alle Tests blieben grün, weil keiner das Manifest auf seine Kennzahlen ansah. Aufgefallen erst, als das Repository **alleinige** Quelle war: Jedes Backup meldete Größe null. Die Engine schreibt jetzt über die Session, deduplizierte Blöcke werden mitvermerkt. - **Fund — der Katalog-Neuaufbau scheiterte bei fehlendem Verzeichnis.** „Katalog verloren" heißt nicht immer „Datei gelöscht". Die bestehenden Tests löschten stets nur die Datei. - **Fund — eine Fortsetzung meldete Teilfehler bei vollständigem Ergebnis.** Die Dateien zwischen Prüfpunkt und tatsächlichem Fortschritt lagen bereits am Platz (85 von 1500) und wurden übergangen. Sie stammen aus dem eigenen abgebrochenen Lauf und werden jetzt ersetzt — die Lockerung gilt ausschließlich bei einer Fortsetzung. - **Fund — die beim Absturz geschriebene Datei blieb dauerhaft liegen** (1501 Dateien in einem Ziel für 1500). Eine Fortsetzung räumt jetzt auf, bevor sie beginnt. - **Fund beim ersten Einspielversuch:** Ein leeres Musterfeld wurde zu NULL und brach an der häufigsten aller Quellen ab — der ohne Filter. Danach gleich der zweite: Die Spalten sind `jsonb`, nicht `text[]`. - Nachgewiesen: `DROP DATABASE` → aus dem Repository wiederaufgebaut → Quelle gelöscht → bitgenau wiederhergestellt. `rm -rf indexes/` → Katalog baut sich selbst neu. SIGKILL bei 496 von 1500 Dateien → Freigabe als `SCHEDULER_LOST` → Fortsetzung → 1500 Dateien, 0 übersprungen, alle Prüfsummen stimmen. Siehe `docs/disaster-recovery.md`. ### Phase 19 (Härtung) **Sicherheit wird nicht behauptet, sondern angegriffen.** Jeder der elf Angriffe lief gegen die laufende Anlage. Abgewehrt wurden: SQL Injection (pgx-Parameter), RBAC (sechs schreibende Zugriffe als Viewer → 403), Rechteausweitung, Authentifizierung, sofortiger Widerruf nach Kontosperre, XSS (Go maskiert `<` in JSON + `nosniff` + CSP), Pfadausbruch aus dem Manifest. Vier Angriffe kamen durch. - **Fund — Wiederherstellung nach `/etc` galt als durchführbar.** Der Angriff braucht keine Lücke: Wer Backups zurückschreiben darf, schreibt nach `/etc/cron.d` oder in eine fremde `authorized_keys`. `recovery.TargetGuard` sperrt die Systemverzeichnisse des **laufenden** Systems; `SYNCOVA_RESTORE_ALLOWED_ROOTS` begrenzt weiter. Der Pfadvergleich läuft über die Trennung, nicht über `HasPrefix` — sonst gälte `/etchen` als Teil von `/etc`. - **Fund — SSRF vollständig offen.** Webhook auf `169.254.169.254` (Metadatendienst der Cloud), `127.0.0.1:5432` (eigene Datenbank), privates Netz. `platform/netguard` prüft an **zwei** Stellen: beim Anlegen (der Betreiber sieht es sofort) und vor dem Verbindungsaufbau (ein Name kann zwischenzeitlich auf eine andere Adresse zeigen — der übliche Weg um eine einmalige Prüfung herum). Eingebettete IPv4-Adressen (`::ffff:127.0.0.1`) werden entpackt, **jede** aufgelöste Adresse geprüft. Auch der SMTP-Server ist ein Ziel. - **Fund — Rate Limit nur beim Login.** 100 von 100 Anfragen liefen durch; teuer sind Bericht (PDF), Vorabprüfung (liest jeden Block) und Security Center (zehn Abfragen). Jetzt allgemeiner Begrenzer in der Middleware-Kette, Vorgabe 600/min. Nachgewiesen: 700 Anfragen → 594 × 200, 106 × 429. - **Fund — kein TLS.** Jetzt über Zertifikatsdateien, mit TLS 1.3 nachgewiesen. **Eine halbe Konfiguration wird beim Start abgelehnt** (sonst startete der Dienst im Klartext, obwohl der Betreiber Verschlüsselung eingerichtet zu haben glaubt). Ohne TLS warnt der Start — bei Bindung an alle Schnittstellen in Großbuchstaben. - **Fund — govulncheck meldete neun Schwachstellen der Go-Standardbibliothek** (Toolchain 1.26.1, behoben in 1.26.5). Nach dem Anheben: null. - **Fund — die Repository-Wurzel war weltlesbar.** `MkdirAll` legt Elternverzeichnisse mit der umask an, während jedes Unterverzeichnis `0700` trug. - **ST1005 ist die einzige abgeschaltete staticcheck-Regel.** Sie verlangt kleingeschriebene Fehlertexte ohne Satzzeichen — eine englische Konvention. Ein Teil der Texte geht unverändert an Anwender; die Regel zu befolgen hieße, Anwendermeldungen zu verstümmeln, damit ein Werkzeug schweigt. - **Der Secret-Scanner ist selbst geschrieben und läuft als Test mit.** Er kennt Platzhalter (sonst meldet er jede Beispielkonfiguration) und gibt einen Fund **nie vollständig** aus — ein Scanner, der das Geheimnis ins Prüfprotokoll schreibt, hat es ein zweites Mal veröffentlicht. Ein zweiter Test prüft, dass er überhaupt anschlägt: Ein Scanner ohne greifende Muster ist von einem sauberen Bestand nicht zu unterscheiden. 14 Fundstellen im Bestand, alle erfundene Testwerte, jetzt mit `secretscan:erlaubt` gekennzeichnet. - Siehe `docs/hardening.md`. ### Phase 20 (Leistungsmessung) **Eine Zahl ohne Messbedingungen ist wertlos — und wird trotzdem zitiert.** Jede Messung trägt Maschine, Datenmenge, Datenbeschaffenheit und Einstellungen bei sich. `Result.Validate()` lehnt einen Durchsatz aus komprimierbaren Daten ab und einen Lauf unter einer halben Sekunde: Der Fehler aus Phase 4 (1021-fache Kompression bei periodischen Testdaten) steht jetzt im Modell, nicht in einer Anleitung. - **Gemessen auf Apple M1, 8 Kerne, lokale SSD, inkompressible Daten, ohne Verschlüsselung:** große Quelle 151,9 MiB/s; zweiter Lauf über unveränderte Daten 662,8 MiB/s; vier gleichzeitige Aufträge 148,4 MiB/s; viele kleine Dateien 107 Dateien/s. - **Der zweite Lauf ist viermal schneller** — unabhängige Bestätigung der Aussage aus Phase 6: Der Gewinn einer Zusatzsicherung ist Zeit, nicht Speicher. - **Bei einer großen Quelle ist die Platte der Engpass, nicht die CPU** (0,7 von 8 Kernen ausgelastet). Mehr Arbeiter brächten nichts. - **Die Streaming-Pipeline hält:** 256 MiB Quelle bei 256 MiB Speichergrenze, Höchststand 195 MiB. - **Fund — 4 MiB Lesepuffer für jede 16-KiB-Datei.** Der Chunker legte ihn stets in Höchstblockgröße an; 4000 Dateien forderten exakt 16 GiB an. Mit `ChunkerOptions.ExpectedSize`: 411 MiB, 90 statt 1447 Bereinigungen, Rechenzeit von 5,44 s auf 1,27 s. - **Der eigentliche Engpass bei kleinen Dateien ist `fsync`.** Direkt gemessen: 113 Dateien/s mit, 4722 ohne — Faktor 42. Die Anlage erreicht 107, ihr Eigenanteil liegt bei rund fünf Prozent. Das ist kein Fehler, sondern der Preis des Commit-Protokolls aus Phase 2. Den Verzeichnis-`fsync` zu bündeln wäre möglich und **wurde bewusst nicht getan**: Der Eingriff verändert die Haltbarkeitszusage und gehört für sich entschieden, nicht nebenbei in einer Messphase. - **Zwei Funde am Messwerkzeug selbst.** Der zweite Aufruf scheiterte an einer vergebenen Backup-Kennung — ein Messwerkzeug, das sich nicht wiederholen lässt, ist keines. Und schlimmer: Bei bestehendem Repository maß „Eine große Quelle" beim zweiten Mal die Deduplizierung statt das Ablegen (641 statt 152 MiB/s), **unbemerkt** — beide Läufe lieferten plausible Zahlen. Aufgefallen allein an der fehlenden Zeile „Abgelegt". - **Nicht gemessen und als solches ausgewiesen:** echte VM (kein Proxmox), langsames Repository (eine selbst vorgegebene Wartezeit misst nichts), Datenträger-IOPS (auf macOS nicht je Prozess auslesbar), Netzdurchsatz (nur lokale Ablage), Verschlüsselungsaufschlag, Repository-Konkurrenz. Siehe `docs/performance.md`. ### Phase 21 (Chaos Testing) **„Every failure must produce a controlled result."** Kontrolliert heißt vier Dinge, und **drei von vier genügen nicht**: Der Fehler wird gemeldet, er ist klassifiziert, es bleibt kein sichtbares unvollständiges Backup zurück, der Zustand danach ist konsistent. Die dritte Bedingung wiegt am schwersten — ein abgebrochener Lauf, der ein halbes Backup als gültig hinterlässt, ist ein Datenverlust mit Zeitzünder. - **Platte voll, mit echtem Dateisystem geprüft** (40 MiB per `hdiutil`, 120 MiB Quelle): Exit 1, klare Meldung, **null Manifeste**, keine halben Dateien. Das Backup wird erst mit dem Manifest sichtbar — es entsteht nie ein scheinbar gültiges. - **Fund — volle Platte wurde als `source` klassifiziert und deshalb wiederholt.** Die Quelle ist aber in Ordnung; jeder Wiederholungslauf legte weitere Blöcke ab und **verschärfte** die Lage. Jetzt `REPOSITORY_FULL` mit Klasse `configuration` (nicht wiederholbar). Erkennung über `errors.Is(err, syscall.ENOSPC)`, nicht über den Meldungstext — ein Textvergleich bräche bei der ersten übersetzten Fehlermeldung, unbemerkt. - **Fund — Datenbankausfall wurde als „Sitzung abgelaufen" gemeldet.** Die Tokenprüfung braucht die Datenbank; fällt sie aus, scheitert jede Prüfung. Der Betreiber meldet sich neu an, was ebenfalls scheitert, und sucht den Fehler bei der Anmeldung. Jetzt `SERVICE_UNAVAILABLE` mit dem Zusatz „Das ist kein Problem Ihrer Sitzung." - **Datenbankausfall im Betrieb:** `/health/live` bleibt 200, `/health/ready` wird 503, der Dienst überlebt und ist 2 s nach Rückkehr der Datenbank wieder bereit — ohne Neustart. Genau das Verhalten aus Phase 0. - **Manifest beschädigt:** Prüfung meldet mit Empfehlung, Wiederherstellung bricht ab (**0 Dateien im Ziel**), Katalog-Neuaufbau schließt es aus. Dass der Katalog es zunächst weiter anzeigt, ist richtig — er ist ein Beschleuniger; verbindlich sind die Manifeste. - **Grenzen:** Netzverlust nicht neu ausgelöst (nur lokale Repositories; Nachweis aus Phase 5 und 18), Stromausfall simuliert (SIGKILL, kein Verlust des Plattenzwischenspeichers), Störungen einzeln statt kombiniert, kein Dauerlauf mit zufälligen Störungen. Siehe `docs/chaos.md`. ### Phase 22 (Release Candidate) **Eingefroren heißt nicht „nicht mehr ändern", sondern „nur noch absichtlich".** Vier Verträge stehen in Dateien, die man anfassen muss, und in Tests, die jede Abweichung melden: `contract_routes.txt` (102 Endpunkte), `migrations/checksums.txt`, ein Container-Fixture und ein Repository-Fixture. Alle vier Prüfungen sind mit Mutationen als fangend nachgewiesen. - **Der API-Vertrag wertet den Quelltext aus, nicht den gebauten Multiplexer.** `http.ServeMux` gibt seine Routen nicht heraus, und ein Test, der die bekannten Adressen anfragt, bemerkt eine **hinzugefügte** nicht — die häufigste Vertragsänderung überhaupt. Die geforderte Berechtigung steht mit in der Zeile: Eine stillschweigend gelockerte Prüfung ist die gefährlichste Änderung, die es gibt, weil der Endpunkt weiter funktioniert. - **Eine ausgelieferte Migration darf sich nie wieder ändern.** Datenbanken, die sie angewandt haben, führen sie nicht erneut aus — die Änderung wirkt ausschließlich auf **neue** Installationen, und es entstehen zwei Schemata mit derselben Versionsnummer. Wer etwas ändern will, schreibt eine neue Migration. - **Fixture-Dateien statt Rundlauftests.** Ein Rundlauf schreibt und liest mit demselben Code und bliebe grün, wenn sich beide Seiten gemeinsam ändern — genau der gefürchtete Fall. Neu erzeugen nur mit `SYNCOVA_WRITE_FIXTURE=ja`. - **Der Upgrade-Test ist der einzige, den es nie gab.** Bisher wurde jedes Schema von Grund auf angelegt; das prüft ausschließlich die Neuinstallation. Eine Spalte mit `NOT NULL` ohne Vorgabewert läuft auf einer leeren Tabelle durch und scheitert auf einer gefüllten — als Mutation nachgewiesen. Eine Absicherung weist einen leeren Ausgangsbestand zurück: Ein Vergleich „vorher gleich nachher" ist auf leeren Tabellen immer erfüllt. - **Rollback: genau ein Schritt, ältere Daten unberührt, Weg nach vorn offen.** Alle Rückrichtungen laufen auf einer **gefüllten** Datenbank bis Version 0 und wieder hoch. Was er nicht kann, steht ausdrücklich im Test: Daten in Tabellen, die es vorher nicht gab, sind danach weg. Ein Rollback ersetzt keine Sicherung der Datenbank. - **Fund im Test selbst:** Die Rollback-Prüfung unterstellte, die Proxmox-Migration sei die letzte. Mit 000013 stimmte das nicht mehr, und der Test meldete einen Fehler, wo keiner war. Er sucht die Tabelle jetzt, statt eine Reihenfolge anzunehmen. - **Fund im Durchlauf — ein Repository ließ sich über die API gar nicht anlegen.** `SYNCOVA_API.md` §8 verlangt sieben Endpunkte, vorhanden war die Liste. Jeder frühere Nachweis hatte die Zeile selbst per SQL eingetragen; deshalb fiel es nie auf. Die Anlage war über ihre eigene API nicht in Betrieb zu nehmen. `POST /repositories` **legt nichts an, sondern übernimmt**: Es öffnet das vorhandene Repository und liest dessen Kennung aus dem Descriptor. - **Fund — der Ort eines Repositorys war nicht eindeutig.** Zwei Einträge auf dasselbe Verzeichnis ergäben Wettlauf um die Schreibsperre und doppelt gezählten Speicher. Migration `000013` setzt die Eindeutigkeit. - **Der Integritätslauf über die API meldet einen Befund nicht als Fehler.** „Die Prüfung schlug fehl" und „das Repository ist beschädigt" sind zwei völlig verschiedene Lagen. - Nachgewiesen: 30 MiB über die API gesichert, Integritätslauf sauber (69 Blöcke), Prüfung mit Wiederherstellungstest `clean` → `recoverable` 70 %, Quelle gelöscht, **61 von 61 Dateien bitgenau** zurück, Symlink und Rechte erhalten. Disaster Recovery auf leerer Datenbank: 11 Repositories, 12 Aufträge, 6 Konten — **0 aktive Aufträge, 0 aktive Konten**. Security 29/88 (`unzureichend`, 8 kritische Befunde — richtig für diese Umgebung). Leistung ohne Regression gegen Phase 20. - **Akzeptanzmatrix: 32 von 38 Zeilen erbracht**, 2 nicht umgesetzt (Windows-Dienst, Capacity Forecast), 4 nur gegen Nachbauten. Alle offenen hängen an fehlender Hardware. Siehe `docs/release-candidate.md`. ### Phase 23 (V1 Release) **Das Paket ist ein Verzeichnisbaum, kein Installationsprogramm.** Ein Betreiber soll sehen können, was er auspackt: `bin/`, `web/`, `migrations/`, `docs/`, `deployment/` — dazu `SHA256SUMS` **im** Paket, damit sich der Inhalt auch dann prüfen lässt, wenn nur er übertragen wurde. `make release` baut alle drei Zielplattformen. - **`CGO_ENABLED=0`.** Ohne den Schalter bindet Go gegen die libc des Bausystems; der Start scheitert dann auf einer älteren Distribution mit einer Meldung über GLIBC, die niemand einem Backupprogramm zuordnet. Real geprüft: Die gebauten Programme laufen in einem leeren `debian:12-slim`. - **macOS wird bewusst nicht ausgeliefert.** Dort wurde entwickelt, aber der Plan nennt es nicht als Ziel — und eine Plattform auszuliefern, für die es kein Betriebskonzept gibt, weckt Erwartungen, die niemand einlöst. Für Windows kommen Agent und `syncova-repo`, nicht der ganze Server: Programme ohne Betriebskonzept auszuliefern, nur weil sie übersetzen, ist dasselbe Versprechen in klein. - **Fund — kein Programm beantwortete `--version` ohne vollständige Konfiguration.** Wer wissen will, welche Fassung auf einem Server liegt, hat in dem Moment womöglich keine Datenbank: frisch ausgepacktes Paket, laufende Störung. Alle acht Programme antworten jetzt vor dem Laden der Konfiguration, und zwar auf `version`, `--version` und `-version` — sich zu merken, welches Programm welche Schreibweise erwartet, ist niemandem zuzumuten. - **Fund — `syncova-repo break-lock` stand in der Störungsdoku und existierte nicht.** `BreakLock` gab es im Kern, aber ohne Kommandozeile; bei einer hängenden Sperre wäre nur das Löschen der Datei von Hand geblieben. Jetzt vorhanden — es nennt zuerst **wer** die Sperre hält (Prozess, Rechner, Zeitpunkt) und verlangt `SYNCOVA_REPO_CONFIRM_BREAK_LOCK=ja`. Eine unlesbare Sperrdatei gilt als Sperre: Sie als „keine" zu melden wäre der gefährlichste Ausgang. - **Jedes in der Doku genannte Kommando wurde gegen die Wirklichkeit gehalten.** Eine Anleitung, die nicht vorhandene Befehle nennt, ist schlimmer als keine — der Leser sucht dann den Fehler bei sich. - **`scripts/release_test.go` prüft die Vollständigkeit** und läuft im gewöhnlichen Testlauf mit: alle zwölf Bestandteile aus §25, jedes gebaute Programm, alle drei Zielplattformen, `CGO_ENABLED=0`. Ein Programm, das gebaut, aber nicht ausgeliefert wird, fiele sonst erst beim Kunden auf. - **Die Änderungsliste nennt das Nichtenthaltene zuerst.** Kapazitätsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute, harte Verknüpfungen — dazu getrennt davon, was gebaut, aber nie auf echter Hardware gefahren wurde. ## API-Konventionen - Basis `/api/v1`, Bearer-Token, `X-Correlation-ID` auf jedem Request. - Erfolg: `{ "data": …, "meta": { "request_id": … } }` — Fehler: `{ "error": { "code": "BACKUP_REPOSITORY_UNAVAILABLE", "message": …, "details": …, "request_id": … } }`. Fehlercodes sind sprechende SCREAMING_SNAKE_CASE-Konstanten. - Pagination `?page=1&page_size=50`, Totals in `meta`. - `Idempotency-Key` für: Job erstellen, Backup starten, Restore erstellen, Repository erstellen, destruktive Konfigurationsänderungen. - RBAC-Rollen: Viewer, Backup Operator, Restore Operator, Security Administrator, Infrastructure Administrator, Auditor, Super Administrator. ## Datenbank-Konventionen UUIDs als öffentliche IDs, alle Zeitstempel `TIMESTAMPTZ` in UTC, Fremdschlüssel verpflichtend, Soft-Delete wo Audit/Historie es verlangt, niemals Passwörter oder rohe Schlüssel speichern (nur Hashes bzw. `*_ref`/`*_ciphertext`). Jede Schemaänderung als versionierte Migration — der Produktionsstart darf das Schema nie stillschweigend verändern. Empfohlene Indizes stehen in `SYNCOVA_DATABASE.md` §17. ## Implementierungsreihenfolge Die Phasenfolge ist bewusst gewählt: **nicht mit dem Dashboard beginnen.** Die erste produktionsreife vertikale Scheibe ist ```text PostgreSQL → Control API → Repository → Backup Engine → Testquelle → Backup → Manifest → Integritätsprüfung → Restore ``` Erst danach kommen Agents (Phase 5/6) und der Proxmox-Provider (Phase 7). Der verpflichtende E2E-Meilenstein von Phase 7 lautet: VM entdecken → sichern → verifizieren → Test-VM löschen → wiederherstellen → booten → validieren. Jede Phase endet mit lauffähiger Software, automatisierten Tests, Doku, Security-Review und messbaren Exit-Kriterien. Priorisierung: **P0** Backup Engine, Repository, Encryption, Integrity, Recovery, Immutability, Security, Proxmox-/Windows-/Linux-Backup — **P1** Web UI, Dashboard, Monitoring, Alerts, Statistiken, Verification, RBAC, MFA, API — **P2** erweiterte Reports, Capacity Forecast, Anomaly Detection, Object Storage, Backup Copy, Synthetic Full. ### Backup-Wizard (Phase 8, Oberfläche) - **Die Logik liegt in `wizardModel.ts`, getrennt von der Maske.** Welcher Schritt vollständig ist und was in die Anfrage wandert, ist reine Berechnung — und damit ohne gerenderte Maske prüfbar. - **Der Entwurf lebt in einem Zustand, nicht in den Eingabefeldern** — sonst wäre jeder Blick zurück ein Datenverlust. Ein noch nicht erreichter Schritt ist nicht anklickbar; er überspränge eine Prüfung. - **Aufbewahrung, Prüfung und Benachrichtigung fragen nichts ab.** Sie erscheinen (der Plan nennt zehn Schritte), aber ohne Eingabefelder — eine Maske, die Werte sammelt, die niemand auswertet, ist ein vorgetäuschtes Funktionsversprechen. Stattdessen steht dort, was ohne diese Einstellung geschieht. - **`accepts_backups` kommt vom Server.** Die Oberfläche müsste sonst wissen, welche Repository-Zustände schreibend sind. Gesperrte Ziele werden gezeigt und begründet, nicht weggelassen. - **Verschlüsselung ist keine Wahl** — der Executor verweigert ohne Schlüssel den Dienst. Eine Schaltfläche zum Abschalten wäre eine Einstellung, die es nicht gibt. - **Die Zeitzone kommt aus dem Browser.** Ohne Angabe rechnet der Server in UTC, und derselbe Auftrag liefe je nach Standort anders. - **Fund:** `Exec` mit mehreren durch Semikolon getrennten Anweisungen führt pgx über das erweiterte Protokoll **nicht** aus — das Aufräumen der Executor-Tests lief ins Leere und ließ Zeilen in der Datenbank liegen. Jede Anweisung einzeln absetzen. ## UI-Konventionen Semantische Farben ausschließlich für Status: Grün = Healthy, Gelb = Warning, Orange = High, Rot = Critical, Blau/Neutral = Information. Kein zusätzlicher Farbeinsatz zur Dekoration. Simple Mode und Advanced Mode werden getrennt gedacht; der Backup-Wizard führt in 10 Schritten von Name bis Create.