// 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 }