syncova-backup/packages/reports/catalog.go
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

243 lines
8.6 KiB
Go

package reports
import (
"fmt"
"strings"
"time"
)
// ReportType ist die Art eines Berichts.
type ReportType string
const (
// TypeDailyBackup ist der Tagesbericht der Sicherungen.
TypeDailyBackup ReportType = "daily_backup"
// TypeWeeklyBackup ist der Wochenbericht der Sicherungen.
TypeWeeklyBackup ReportType = "weekly_backup"
// TypeMonthlyBackup ist der Monatsbericht der Sicherungen.
TypeMonthlyBackup ReportType = "monthly_backup"
// TypeFailedBackup listet die gescheiterten Laeufe.
TypeFailedBackup ReportType = "failed_backup"
// TypeRepositoryCapacity beschreibt Belegung und Zustand der Repositories.
TypeRepositoryCapacity ReportType = "repository_capacity"
// TypeRecovery beschreibt die Wiederherstellungen.
TypeRecovery ReportType = "recovery"
// TypeSecurity ist der Sicherheitsbericht.
TypeSecurity ReportType = "security"
// TypeCompliance liefert technische Angaben fuer Pruefungen.
TypeCompliance ReportType = "compliance"
// TypeRecoveryObjectives vergleicht RPO- und RTO-Vorgaben mit der Wirklichkeit.
TypeRecoveryObjectives ReportType = "rpo_rto"
)
// Format ist ein Ausgabeformat.
type Format string
const (
// FormatJSON ist die maschinenlesbare Ausgabe.
FormatJSON Format = "json"
// FormatCSV ist die Ausgabe fuer Tabellenkalkulationen.
FormatCSV Format = "csv"
// FormatPDF ist die Ausgabe zum Ablegen und Weiterreichen.
FormatPDF Format = "pdf"
)
// ContentType liefert den MIME-Typ eines Formats.
func (format Format) ContentType() string {
switch format {
case FormatCSV:
return "text/csv; charset=utf-8"
case FormatPDF:
return "application/pdf"
default:
return "application/json; charset=utf-8"
}
}
// FileExtension liefert die Dateiendung eines Formats.
func (format Format) FileExtension() string {
return string(format)
}
// ParseFormat liest ein Ausgabeformat.
func ParseFormat(rawFormat string) (Format, error) {
switch Format(strings.ToLower(strings.TrimSpace(rawFormat))) {
case FormatJSON, "":
return FormatJSON, nil
case FormatCSV:
return FormatCSV, nil
case FormatPDF:
return FormatPDF, nil
default:
return "", fmt.Errorf("das format %q ist unbekannt (erlaubt: json, csv, pdf)", rawFormat)
}
}
// PeriodKind beschreibt, wie der Zeitraum eines Berichts zustande kommt.
type PeriodKind string
const (
// PeriodRange verlangt einen vom Aufrufer gewaehlten Zeitraum.
PeriodRange PeriodKind = "range"
// PeriodPointInTime beschreibt den Zustand jetzt, nicht einen Verlauf.
//
// Der Unterschied ist keine Formalie: Ein Bericht ueber die Belegung der
// Repositories „vom letzten Dienstag" kann es nicht geben — die Belegung
// wird nicht historisiert. Wer nach einem Zeitraum fragt, bekaeme sonst
// einen Zustandsbericht mit falscher Ueberschrift.
PeriodPointInTime PeriodKind = "point_in_time"
)
// Definition beschreibt eine Reportart.
type Definition struct {
// Type ist der maschinenlesbare Bezeichner.
Type ReportType `json:"type"`
// Title ist die Bezeichnung.
Title string `json:"title"`
// Description erklaert, welche Frage der Bericht beantwortet.
Description string `json:"description"`
// PeriodKind beschreibt die Art des Zeitbezugs.
PeriodKind PeriodKind `json:"period_kind"`
// DefaultPeriod ist der Zeitraum ohne ausdrueckliche Angabe.
DefaultPeriod time.Duration `json:"-"`
// DefaultPeriodLabel beschreibt den Standardzeitraum.
DefaultPeriodLabel string `json:"default_period,omitempty"`
// Formats sind die verfuegbaren Ausgabeformate.
Formats []Format `json:"formats"`
}
// allFormats sind die drei Ausgaben, die jeder Bericht beherrscht.
//
// Es gibt keinen Bericht, der nur in einem Format existiert: Ein Format, das
// bei sieben von neun Berichten fehlt, ist im Menue nicht erklaerbar.
var allFormats = []Format{FormatJSON, FormatCSV, FormatPDF}
// Definitions liefert alle Reportarten in Anzeigereihenfolge.
func Definitions() []Definition {
return []Definition{
{
Type: TypeDailyBackup,
Title: "Tagesbericht Sicherungen",
Description: "Alle Sicherungsläufe eines Tages mit Ergebnis, Datenmenge und Dauer. Beantwortet die Frage, die morgens zuerst gestellt wird: Ist heute Nacht alles durchgelaufen?",
PeriodKind: PeriodRange,
DefaultPeriod: 24 * time.Hour,
DefaultPeriodLabel: "letzte 24 Stunden",
Formats: allFormats,
},
{
Type: TypeWeeklyBackup,
Title: "Wochenbericht Sicherungen",
Description: "Die Läufe einer Woche, zusätzlich nach Tagen aufgeschlüsselt. Zeigt Muster, die im Tagesbericht untergehen — etwa einen Auftrag, der immer freitags scheitert.",
PeriodKind: PeriodRange,
DefaultPeriod: 7 * 24 * time.Hour,
DefaultPeriodLabel: "letzte 7 Tage",
Formats: allFormats,
},
{
Type: TypeMonthlyBackup,
Title: "Monatsbericht Sicherungen",
Description: "Die Läufe eines Monats mit Aufschlüsselung je Auftrag. Die übliche Grundlage für eine Berichterstattung nach oben.",
PeriodKind: PeriodRange,
DefaultPeriod: 30 * 24 * time.Hour,
DefaultPeriodLabel: "letzte 30 Tage",
Formats: allFormats,
},
{
Type: TypeFailedBackup,
Title: "Bericht gescheiterte Sicherungen",
Description: "Jeder gescheiterte Lauf und jeder Teilfehler einzeln, mit Fehlerklasse und Fehlercode. Teilfehler stehen ausdrücklich mit darin: Sie werden nicht wiederholt und verschwinden sonst aus dem Blick.",
PeriodKind: PeriodRange,
DefaultPeriod: 7 * 24 * time.Hour,
DefaultPeriodLabel: "letzte 7 Tage",
Formats: allFormats,
},
{
Type: TypeRepositoryCapacity,
Title: "Bericht Repository-Kapazität",
Description: "Belegung, Zustand und Löschschutz aller Repositories. Ein Zustandsbericht, kein Verlauf — die Belegung wird nicht historisiert.",
PeriodKind: PeriodPointInTime,
Formats: allFormats,
},
{
Type: TypeRecovery,
Title: "Wiederherstellungsbericht",
Description: "Alle Wiederherstellungen eines Zeitraums mit Dauer, Datenmenge und Ergebnis. Die einzige Quelle für tatsächlich gemessene Wiederherstellungszeiten.",
PeriodKind: PeriodRange,
DefaultPeriod: 30 * 24 * time.Hour,
DefaultPeriodLabel: "letzte 30 Tage",
Formats: allFormats,
},
{
Type: TypeSecurity,
Title: "Sicherheitsbericht",
Description: "Die Sicherheitsbewertung mit allen Befunden aus dem Security Center. Ein Zustandsbericht: Er beschreibt die Lage jetzt.",
PeriodKind: PeriodPointInTime,
Formats: allFormats,
},
{
Type: TypeCompliance,
Title: "Bericht für Prüfungen",
Description: "Technische Angaben, die bei einer Prüfung verlangt werden: Verschlüsselungsgrad, Löschschutz, zweiter Faktor, Prüfquote, Protokollierung. Dies ist keine Zertifizierung und ersetzt keine.",
PeriodKind: PeriodPointInTime,
Formats: allFormats,
},
{
Type: TypeRecoveryObjectives,
Title: "Bericht RPO und RTO",
Description: "Je Auftrag die zugesagte Wiederherstellungslage gegen die gemessene. RPO wird laufend gemessen; RTO nur dort, wo eine Wiederherstellung tatsächlich stattgefunden hat.",
PeriodKind: PeriodPointInTime,
Formats: allFormats,
},
}
}
// FindDefinition sucht eine Reportart.
func FindDefinition(reportType ReportType) (Definition, bool) {
for _, definition := range Definitions() {
if definition.Type == reportType {
return definition, true
}
}
return Definition{}, false
}
// ParseType liest eine Reportart.
func ParseType(rawType string) (ReportType, error) {
reportType := ReportType(strings.ToLower(strings.TrimSpace(rawType)))
if _, found := FindDefinition(reportType); !found {
return "", fmt.Errorf("die reportart %q ist unbekannt", rawType)
}
return reportType, nil
}
// ResolvePeriod bestimmt den Zeitraum eines Berichts.
//
// Ein Zustandsbericht bekommt den Erzeugungszeitpunkt als beide Grenzen: Ihm
// einen Zeitraum zu geben, den er nicht auswertet, waere die freundlichste Art
// zu luegen — der Leser glaubte, eine Auswahl getroffen zu haben.
func ResolvePeriod(definition Definition, requestedFrom, requestedTo *time.Time, now time.Time) (time.Time, time.Time, error) {
if definition.PeriodKind == PeriodPointInTime {
return now, now, nil
}
periodTo := now
if requestedTo != nil {
periodTo = *requestedTo
}
periodFrom := periodTo.Add(-definition.DefaultPeriod)
if requestedFrom != nil {
periodFrom = *requestedFrom
}
if !periodFrom.Before(periodTo) {
return time.Time{}, time.Time{},
fmt.Errorf("der beginn des zeitraums liegt nicht vor seinem ende")
}
return periodFrom.UTC(), periodTo.UTC(), nil
}