syncova-backup/docs/architecture.md
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

111 lines
5.8 KiB
Markdown

# Aufbau des Codes
Dieses Dokument beschreibt den **aktuell implementierten** Stand. Die angestrebte Gesamtarchitektur steht in [SYNCOVA_ARCHITECTURE.md](../SYNCOVA_ARCHITECTURE.md).
## Schichtung
```text
apps/web Weboberfläche (React + TypeScript)
| HTTP (gleicher Ursprung, im Entwicklungsbetrieb über Proxy)
v
apps/api Control-Plane-API (Go)
|
v
packages/platform Querschnittsdienste: config, logging, database, health
|
v
PostgreSQL ausschließlich Control Plane, niemals Backup-Nutzdaten
```
Die Abhängigkeitsrichtung ist strikt: `packages/` kennt `apps/` nicht. Damit können Agent und Worker später dieselben Querschnittsdienste nutzen, ohne die API einzubinden.
## packages/platform
| Paket | Verantwortung |
| --- | --- |
| `config` | Konfiguration aus Umgebungsvariablen, vollständige Validierung |
| `logging` | strukturiertes Logging, Correlation IDs, Secret-Redaction |
| `database` | PostgreSQL-Pool, Health-Check, Migrationsverwaltung |
| `health` | Health Engine über alle Komponenten |
### config
Alle Werte stammen aus `SYNCOVA_*`-Umgebungsvariablen. Zwei Entscheidungen prägen das Paket:
- **Secrets besitzen keinen Standardwert.** Das Datenbankpasswort ist als nicht exportiertes Feld abgelegt, damit es nicht versehentlich serialisiert wird. Zugriff besteht nur über `ConnectionString()` (mit Passwort) und `RedactedConnectionString()` (ohne).
- **Validierungsfehler werden gesammelt.** `Load()` meldet alle Probleme gemeinsam, statt beim ersten abzubrechen.
Sicherheitsrelevante Voreinstellungen: Listener nur auf `127.0.0.1`, CORS leer, Body-Limit 1 MiB, Wildcard-Herkunft grundsätzlich unzulässig, unverschlüsselte Herkunft in der Produktion abgelehnt.
### logging
`slog` mit JSON-Ausgabe. Die Redaction hängt an `ReplaceAttr` und greift damit für **jedes** Attribut — der einzige Ort, an dem sie nicht vergessen werden kann. Erkannt werden Feldnamen, die auf ein Geheimnis hindeuten (`password`, `token`, `secret`, `connection_string` und weitere), unabhängig von Schreibweise und Präfix.
### health
Jede Komponente meldet einen expliziten Zustand; der Gesamtzustand ist der schlechteste Einzelzustand. Drei Eigenschaften sind bewusst so gebaut:
- Eine Prüfung ohne gemeldeten Zustand gilt als **kritisch**, nicht als gesund.
- Eine Zeitüberschreitung gilt als kritisch, statt den Endpunkt zu blockieren.
- Ein Panic in einer Prüfung wird abgefangen und als kritisch gemeldet.
Nur als kritisch registrierte Komponenten beeinflussen die Readiness. Eine ausgefallene Nebenkomponente nimmt den Dienst also nicht aus dem Verkehr, erscheint aber weiterhin ehrlich im Bericht.
## apps/api
### Middleware-Reihenfolge
```text
Recovery → Correlation → SecurityHeaders → CORS → BodyLimit → AccessLog → Router
```
Recovery liegt außen, damit auch Fehler innerer Schichten erfasst werden. Correlation folgt unmittelbar, damit jede weitere Schicht bereits eine ID zum Loggen hat.
### Request-ID und Correlation ID
Die **Request-ID** wird immer serverseitig als UUID vergeben. Die **Correlation ID** darf der Aufrufer über `X-Correlation-ID` vorgeben, um eine Operation über Systemgrenzen zu verfolgen — aber nur, wenn der Wert eine gültige UUID ist. Andernfalls wird er verworfen, damit keine fremden Zeichenketten in die Logs gelangen.
### Antworthülle
```json
{ "data": {}, "meta": { "request_id": "uuid" } }
{ "error": { "code": "...", "message": "...", "request_id": "uuid" } }
```
`APIError` trennt die ausgelieferte Darstellung von der internen Ursache: Letztere wird über `WithCause` mitgeführt, erscheint im Log und **nie** in der Antwort.
Maßgeblich für den Client ist die Hülle, nicht der HTTP-Status. Beide tragen unterschiedliche Aussagen: der Status beschreibt den Betriebszustand, die Hülle den Inhalt. `GET /api/v1/health` nutzt genau das und meldet einen kritischen Systemzustand mit **503**, liefert dabei aber den vollständigen Bericht als Nutzlast — das externe Monitoring schlägt an, und die Oberfläche kann trotzdem anzeigen, *was* kaputt ist.
### Health-Endpunkte
| Endpunkt | Prüft | Zweck |
| --- | --- | --- |
| `GET /health/live` | nur den Prozess | Neustartentscheidung |
| `GET /health/ready` | alle kritischen Komponenten | Verkehrszuteilung |
| `GET /api/v1/health` | alle Komponenten einzeln | Diagnose |
Liveness prüft bewusst **keine** Abhängigkeiten: sonst würde eine kurzzeitig nicht erreichbare Datenbank einen Neustart des Dienstes auslösen und das Problem verschlimmern.
## Datenbank
PostgreSQL ist die Control-Plane-Datenbank und enthält niemals Backup-Nutzdaten. Beide Zugriffswege — Verbindungspool und Migrationsläufe — nutzen denselben `pgx`-Treiber. Das ist eine bewusste Festlegung: `golang-migrate` würde sonst seinen eingebauten `lib/pq`-Treiber verwenden, der andere SSL-Modi kennt, womit dieselbe Konfiguration je nach Kommando funktionieren oder scheitern würde.
Migrationen sind über `embed` ins Binary eingebettet und laufen ausschließlich über `syncova-migrate`. Der API-Dienst prüft beim Start nur, ob der Schemastand zur Programmversion passt.
## apps/web
Aufbau nach PROMPT.md §104:
```text
src/
api/ HTTP-Client mit Fehlerbehandlung
components/ wiederverwendbare Bausteine des Designsystems
features/ fachliche Bereiche (aktuell: health)
styles/ Design-Token
types/ Typen des API-Vertrags
```
Der Client `requestApi` kapselt die Antworthülle und liefert Fehler stets als `ApiError`. Eine Antwort außerhalb des Vertrags führt zu einem klaren Fehler statt zu einem stillschweigend leeren Ergebnis.
Die Statusfarben (grün/gelb/orange/rot/blau) sind ausschließlich Zuständen vorbehalten. Ein Zustand wird nie allein über Farbe vermittelt, sondern immer zusätzlich als Text — Farbe allein wäre für farbfehlsichtige Anwender unzugänglich.