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

192 lines
6.5 KiB
Go

// Package reports erzeugt Berichte ueber Sicherung, Wiederherstellung und
// Sicherheitslage (SYNCOVA_IMPLEMENTATION_PLAN.md §19, PROMPT.md §72).
//
// Zwei Entscheidungen tragen dieses Paket:
//
// Erstens ist das Reportmodell **formatunabhaengig**. Ein Report ist eine
// Sammlung aus Kennzahlen und Tabellen; CSV, JSON und PDF sind drei Sichten auf
// dieselbe Struktur. Waeren die Formate in die Berichte eingebaut, muesste jeder
// der neun Berichte dreimal geschrieben werden — und beim zehnten vergisst
// jemand eines davon.
//
// Zweitens traegt **jede Kennzahl mit sich, ob sie gemessen wurde**. Ein
// Zeitraum ohne Sicherungslauf hat keine Erfolgsquote — nicht hundert Prozent
// und nicht null. Ein Bericht ist das Dokument, das aus der Anlage herausgeht:
// Er landet in einer Tabellenkalkulation, in einer Praesentation und in einem
// Auditordner. Eine erfundene Null ueberlebt dort jede muendliche Erlaeuterung.
package reports
import (
"fmt"
"time"
)
// Unit ist die Einheit einer Kennzahl.
//
// Sie steht am Wert, nicht in der Beschriftung: Eine CSV-Spalte „Datenmenge"
// ohne Einheit zwingt den Leser zum Raten, und geraten wird meistens falsch.
type Unit string
const (
// UnitNone ist eine blosse Zahl.
UnitNone Unit = ""
// UnitBytes ist eine Datenmenge in Byte.
UnitBytes Unit = "bytes"
// UnitSeconds ist eine Dauer in Sekunden.
UnitSeconds Unit = "seconds"
// UnitPercent ist ein Anteil in Prozent.
UnitPercent Unit = "percent"
// UnitCount ist eine Anzahl.
UnitCount Unit = "count"
// UnitBytesPerSecond ist ein Durchsatz.
UnitBytesPerSecond Unit = "bytes_per_second"
)
// Metric ist eine einzelne Kennzahl eines Berichts.
type Metric struct {
// Label ist die Beschriftung.
Label string `json:"label"`
// Value ist der gemessene Wert; nur gueltig, wenn IsKnown gilt.
Value float64 `json:"value"`
// Unit ist die Einheit.
Unit Unit `json:"unit,omitempty"`
// Text ist eine nicht numerische Angabe, etwa „aktiv".
Text string `json:"text,omitempty"`
// IsKnown meldet, ob der Wert gemessen wurde.
//
// Ohne dieses Feld gaebe es keinen Unterschied zwischen „null gemessen" und
// „nicht gemessen" — und genau dieser Unterschied entscheidet, ob ein
// Bericht die Wahrheit sagt.
IsKnown bool `json:"is_known"`
// UnknownReason erklaert einen fehlenden Wert.
//
// Pflicht, sobald IsKnown nicht gilt: Eine leere Zelle ohne Begruendung
// sieht aus wie ein Fehler des Werkzeugs.
UnknownReason string `json:"unknown_reason,omitempty"`
}
// KnownMetric erzeugt eine gemessene Kennzahl.
func KnownMetric(label string, value float64, unit Unit) Metric {
return Metric{Label: label, Value: value, Unit: unit, IsKnown: true}
}
// TextMetric erzeugt eine nicht numerische Angabe.
func TextMetric(label string, text string) Metric {
return Metric{Label: label, Text: text, IsKnown: true}
}
// UnknownMetric erzeugt eine ungemessene Kennzahl mit Begruendung.
func UnknownMetric(label string, unit Unit, reason string) Metric {
return Metric{Label: label, Unit: unit, IsKnown: false, UnknownReason: reason}
}
// Display liefert die Darstellung einer Kennzahl fuer Menschen.
func (metric Metric) Display() string {
if !metric.IsKnown {
return "nicht gemessen"
}
if metric.Text != "" {
return metric.Text
}
switch metric.Unit {
case UnitBytes:
return FormatBytes(metric.Value)
case UnitSeconds:
return FormatDuration(metric.Value)
case UnitPercent:
return FormatPercent(metric.Value)
case UnitBytesPerSecond:
return FormatBytes(metric.Value) + "/s"
case UnitCount:
return fmt.Sprintf("%.0f", metric.Value)
default:
return trimNumber(metric.Value)
}
}
// Table ist eine Tabelle innerhalb eines Berichts.
type Table struct {
// Title ist die Ueberschrift.
Title string `json:"title"`
// Columns sind die Spaltenbeschriftungen.
Columns []string `json:"columns"`
// Rows sind die Zeilen; jede Zelle ist bereits als Text aufbereitet.
//
// Text und nicht ein beliebiger Wert: Die Aufbereitung einer Datenmenge
// gehoert zum Bericht, nicht zum Ausgabeformat — sonst formatierten CSV und
// PDF dieselbe Zahl unterschiedlich.
Rows [][]string `json:"rows"`
// EmptyNotice steht anstelle der Tabelle, wenn es keine Zeilen gibt.
//
// „Keine Eintraege" ist eine Aussage, eine leere Flaeche ist keine.
EmptyNotice string `json:"empty_notice,omitempty"`
}
// Section ist ein Abschnitt eines Berichts.
type Section struct {
// Title ist die Ueberschrift.
Title string `json:"title"`
// Description erklaert den Abschnitt in einem Satz.
Description string `json:"description,omitempty"`
// Metrics sind die Kennzahlen.
Metrics []Metric `json:"metrics,omitempty"`
// Tables sind die Tabellen.
Tables []Table `json:"tables,omitempty"`
}
// Report ist ein fertiger Bericht.
type Report struct {
// Type ist die Reportart.
Type ReportType `json:"type"`
// Title ist die Bezeichnung.
Title string `json:"title"`
// Description erklaert, was der Bericht zeigt.
Description string `json:"description"`
// PeriodFrom ist der Beginn des Zeitraums in UTC.
PeriodFrom time.Time `json:"period_from"`
// PeriodTo ist das Ende des Zeitraums in UTC.
PeriodTo time.Time `json:"period_to"`
// GeneratedAt ist der Erzeugungszeitpunkt in UTC.
//
// Ein Bericht ohne Erzeugungszeitpunkt ist wertlos: Niemand kann sagen, ob
// er die Lage von heute oder die vom letzten Quartal beschreibt.
GeneratedAt time.Time `json:"generated_at"`
// GeneratedBy ist der Anmeldename des Anfordernden.
GeneratedBy string `json:"generated_by,omitempty"`
// Sections sind die Abschnitte.
Sections []Section `json:"sections"`
// Notes benennen Luecken und Einschraenkungen.
//
// Sie stehen im Bericht selbst, nicht in einer Betriebsanleitung: Wer den
// Bericht in einem halben Jahr aus einem Ordner zieht, hat die Anleitung
// nicht dabei.
Notes []string `json:"notes,omitempty"`
}
// AddNote ergaenzt einen Hinweis.
//
// Der Text traegt **keine** Auszeichnung — keine Sternchen, keine Klammern, kein
// Markdown. Er erscheint unveraendert in CSV, JSON und PDF; ein Sternchenpaar,
// das im Browser fett wuerde, steht in der PDF-Datei woertlich da und in der
// Tabellenkalkulation ebenso.
func (report *Report) AddNote(noteFormat string, formatArguments ...any) {
report.Notes = append(report.Notes, fmt.Sprintf(noteFormat, formatArguments...))
}
// UnknownMetricCount zaehlt die ungemessenen Kennzahlen.
func (report *Report) UnknownMetricCount() int {
unknownCount := 0
for _, section := range report.Sections {
for _, metric := range section.Metrics {
if !metric.IsKnown {
unknownCount++
}
}
}
return unknownCount
}