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

143 lines
4.2 KiB
Go

package reports
// Zeichenbreiten der Schrift Helvetica in 1/1000 em.
//
// Die Werte stammen aus den Metriken der Standardschrift. Ohne sie liesse sich
// kein Zeilenumbruch rechnen und keine Tabellenspalte bemessen: Eine geschaetzte
// Durchschnittsbreite laesst „Wiederherstellung fehlgeschlagen" in die
// Nachbarspalte laufen, waehrend „111111" viel Platz uebrig liesse.
//
// Der Index ist der Zeichenwert minus 32; abgedeckt ist 32 bis 126.
var helveticaWidths = [95]int{
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, // 32-41
389, 584, 278, 333, 278, 278, 556, 556, 556, 556, // 42-51
556, 556, 556, 556, 556, 556, 278, 278, 584, 584, // 52-61
584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, // 62-71
722, 278, 500, 667, 556, 833, 722, 778, 667, 778, // 72-81
722, 667, 611, 722, 667, 944, 667, 667, 611, 278, // 82-91
278, 278, 469, 556, 333, 556, 556, 500, 556, 556, // 92-101
278, 556, 556, 222, 222, 500, 222, 833, 556, 556, // 102-111
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, // 112-121
500, 334, 260, 334, 584, // 122-126
}
// boldWidthFactor naehert die Breite der fetten Schnitte an.
//
// Helvetica-Bold ist rund drei Prozent breiter als Helvetica. Eine zweite
// vollstaendige Tabelle dafuer waere Aufwand ohne Gewinn: Fett gesetzt sind hier
// nur Ueberschriften und Spaltenkoepfe, und die stehen fuer sich.
const boldWidthFactor = 1.03
// defaultGlyphWidth ist die angenommene Breite eines Zeichens ausserhalb der Tabelle.
//
// Umlaute liegen in dieser Schrift bei rund 556; das ist deutlich naeher an der
// Wahrheit als der Rueckgriff auf null, der eine Zeile beliebig lang erscheinen
// liesse.
const defaultGlyphWidth = 556
// textWidth misst einen Text in Punkten.
func textWidth(text string, font pdfFont, fontSize float64) float64 {
totalWidth := 0
for _, textRune := range text {
glyphIndex := int(textRune) - 32
if glyphIndex >= 0 && glyphIndex < len(helveticaWidths) {
totalWidth += helveticaWidths[glyphIndex]
continue
}
totalWidth += defaultGlyphWidth
}
measuredWidth := float64(totalWidth) * fontSize / 1000
if font == fontBold {
return measuredWidth * boldWidthFactor
}
return measuredWidth
}
// wrapText bricht einen Text auf die vorgegebene Breite um.
//
// Umgebrochen wird an Leerzeichen. Ein Wort, das allein schon zu breit ist,
// bleibt ungebrochen stehen und ragt heraus — es zeichenweise zu zerlegen
// zerrisse Pfade und Kennungen genau dort, wo sie gelesen werden muessen.
func wrapText(text string, font pdfFont, fontSize float64, availableWidth float64) []string {
words := splitWords(text)
if len(words) == 0 {
return []string{""}
}
wrappedLines := make([]string, 0, 4)
currentLine := words[0]
for _, word := range words[1:] {
candidateLine := currentLine + " " + word
if textWidth(candidateLine, font, fontSize) <= availableWidth {
currentLine = candidateLine
continue
}
wrappedLines = append(wrappedLines, currentLine)
currentLine = word
}
return append(wrappedLines, currentLine)
}
// splitWords zerlegt einen Text an Leerzeichen.
func splitWords(text string) []string {
words := make([]string, 0, 16)
currentWord := make([]rune, 0, 16)
for _, textRune := range text {
if textRune == ' ' || textRune == '\t' || textRune == '\n' {
if len(currentWord) > 0 {
words = append(words, string(currentWord))
currentWord = currentWord[:0]
}
continue
}
currentWord = append(currentWord, textRune)
}
if len(currentWord) > 0 {
words = append(words, string(currentWord))
}
return words
}
// truncateToWidth kuerzt einen Text auf die vorgegebene Breite.
//
// Gekuerzt wird mit Auslassungspunkten, damit erkennbar bleibt, dass etwas
// fehlt. Ein stillschweigend abgeschnittener Pfad sieht aus wie ein anderer
// Pfad.
func truncateToWidth(text string, font pdfFont, fontSize float64, availableWidth float64) string {
if textWidth(text, font, fontSize) <= availableWidth {
return text
}
const ellipsis = "…"
textRunes := []rune(text)
ellipsisWidth := textWidth(ellipsis, font, fontSize)
for runeCount := len(textRunes) - 1; runeCount > 0; runeCount-- {
candidate := string(textRunes[:runeCount])
if textWidth(candidate, font, fontSize)+ellipsisWidth <= availableWidth {
return candidate + ellipsis
}
}
return ellipsis
}