package internal
import (
"fmt"
"html"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// persistHTMLReport generates and persists the HTML status page.
// Mirrors persistReport() pattern from run.go with atomic write.
func persistHTMLReport(state state, subject string, conf config) error {
htmlFile := conf.HTMLStatusFile
if htmlFile == "" {
log.Println("debug: HTMLStatusFile is empty, skipping HTML report generation")
return nil
}
log.Println("debug: HTMLStatusFile set to", htmlFile)
htmlDir := filepath.Dir(htmlFile)
// Auto-create directory if it doesn't exist
// CLAUDE: Only create it when it doesnt exist yet
if err := os.MkdirAll(htmlDir, 0o755); err != nil {
log.Println("debug: error creating directory:", err)
return fmt.Errorf("failed to create directory %s: %w", htmlDir, err)
}
log.Println("debug: directory ensured at", htmlDir)
tmpFile := htmlFile + ".tmp"
log.Println("debug: writing to temp file", tmpFile)
f, err := os.Create(tmpFile)
if err != nil {
log.Println("debug: error creating temp file:", err)
return fmt.Errorf("failed to create temp file: %w", err)
}
defer f.Close()
htmlContent := state.htmlReport(subject)
if _, err = f.WriteString(htmlContent); err != nil {
log.Println("debug: error writing HTML:", err)
return fmt.Errorf("failed to write HTML: %w", err)
}
log.Println("debug: successfully wrote HTML to temp file")
err = os.Rename(tmpFile, htmlFile)
if err != nil {
log.Println("debug: error renaming temp file to final location:", err)
return err
}
log.Println("debug: successfully renamed and persisted HTML report to", htmlFile)
return nil
}
// htmlReport generates the complete HTML status page.
// Mirrors state.report() pattern from state.go:133-163.
func (s state) htmlReport(subject string) string {
var sb strings.Builder
// Calculate counts for header summary (without generating HTML yet)
numCriticals := s.countBy(func(cs checkState) bool {
return cs.Status == nagiosCritical
})
numWarnings := s.countBy(func(cs checkState) bool {
return cs.Status == nagiosWarning
})
numUnknown := s.countBy(func(cs checkState) bool {
return cs.Status == nagiosUnknown
})
numOK := s.countBy(func(cs checkState) bool {
return cs.Status == nagiosOk
})
numStale := s.countStale()
// Write HTML header with summary
sb.WriteString(htmlHeader(subject, numCriticals, numWarnings, numUnknown, numStale, numOK))
// Alerts with status changed section
sb.WriteString(`
` + "\n")
sb.WriteString(`
Alerts with status changed
` + "\n")
changed := s.htmlReportChanged(&sb)
if !changed {
sb.WriteString(`
There were no status changes...
` + "\n")
}
sb.WriteString(`
` + "\n\n")
// Unhandled alerts section
sb.WriteString(`` + "\n")
sb.WriteString(`
Unhandled alerts
` + "\n")
hasUnhandled := (numCriticals + numWarnings + numUnknown) > 0
if hasUnhandled {
s.htmlReportUnhandledContent(&sb)
} else {
sb.WriteString(`
There are no unhandled alerts...
` + "\n")
}
sb.WriteString(`
` + "\n\n")
// Stale alerts section
sb.WriteString(`` + "\n")
sb.WriteString(`
Stale alerts
` + "\n")
if numStale == 0 {
sb.WriteString(`
There are no stale alerts...
` + "\n")
} else {
s.htmlReportStaleAlerts(&sb)
}
sb.WriteString(`
` + "\n\n")
// OK checks section
sb.WriteString(`` + "\n")
sb.WriteString(`
OK checks
` + "\n")
if numOK == 0 {
sb.WriteString(`
There are no OK checks...
` + "\n")
} else {
s.htmlReportBy(&sb, false, false, func(cs checkState) bool {
return cs.Status == nagiosOk
})
}
sb.WriteString(`
` + "\n\n")
sb.WriteString(htmlFooter())
return sb.String()
}
// htmlReportChanged generates HTML for checks with status changes.
// Mirrors state.reportChanged() from state.go:166-192.
func (s state) htmlReportChanged(sb *strings.Builder) (changed bool) {
if 0 < s.htmlReportBy(sb, true, false, func(cs checkState) bool {
return cs.Status == nagiosCritical && cs.changed()
}) {
changed = true
}
if 0 < s.htmlReportBy(sb, true, false, func(cs checkState) bool {
return cs.Status == nagiosWarning && cs.changed()
}) {
changed = true
}
if 0 < s.htmlReportBy(sb, true, false, func(cs checkState) bool {
return cs.Status == nagiosUnknown && cs.changed()
}) {
changed = true
}
if 0 < s.htmlReportBy(sb, true, false, func(cs checkState) bool {
return cs.Status == nagiosOk && cs.changed()
}) {
changed = true
}
return
}
// htmlReportUnhandledContent generates HTML content for unhandled alerts section.
// Mirrors state.reportUnhandled() from state.go:194-214.
func (s state) htmlReportUnhandledContent(sb *strings.Builder) {
s.htmlReportBy(sb, false, false, func(cs checkState) bool {
return cs.Status == nagiosCritical
})
s.htmlReportBy(sb, false, false, func(cs checkState) bool {
return cs.Status == nagiosWarning
})
s.htmlReportBy(sb, false, false, func(cs checkState) bool {
return cs.Status == nagiosUnknown
})
}
// htmlReportStaleAlerts generates HTML for stale checks.
// Only reports stale alerts that are not OK, since stale OK alerts aren't concerning.
// Mirrors state.reportStaleAlerts() from state.go.
func (s state) htmlReportStaleAlerts(sb *strings.Builder) int {
return s.htmlReportBy(sb, false, true, func(cs checkState) bool {
return cs.Epoch < s.staleEpoch && cs.Status != nagiosOk
})
}
// htmlReportBy is the generic HTML generator for check items.
// Mirrors state.reportBy() from state.go:222-262 but outputs HTML.
func (s state) htmlReportBy(sb *strings.Builder, showStatusChange, isStaleReport bool,
filter func(cs checkState) bool,
) (count int) {
for name, cs := range s.checks {
if !filter(cs) {
continue
}
if !isStaleReport && cs.Epoch < s.staleEpoch {
continue // skip stale checks in non-stale report
}
count++
sb.WriteString(`` + "\n")
// Show status change if applicable
if showStatusChange && cs.changed() {
sb.WriteString(htmlStatusBadge(nagiosCode(cs.PrevStatus)))
sb.WriteString(` → `)
}
// Show current status
sb.WriteString(htmlStatusBadge(nagiosCode(cs.Status)))
sb.WriteString(": ")
sb.WriteString(html.EscapeString(name))
sb.WriteString(": ")
sb.WriteString(html.EscapeString(cs.Output))
// Show federated source if applicable
if cs.federated() {
sb.WriteString(" [federated from ")
sb.WriteString(html.EscapeString(cs.FederatedFrom))
sb.WriteString("]")
}
// Show stale duration if applicable
if isStaleReport {
lastCheckedAgo := time.Since(time.Unix(cs.Epoch, 0))
sb.WriteString(fmt.Sprintf(" (last checked %v ago)", lastCheckedAgo))
}
sb.WriteString("\n
\n")
}
return
}
// countStale counts the number of stale checks (excluding OK status).
// Helper function for generating summary counts.
func (s state) countStale() int {
return s.countBy(func(cs checkState) bool {
return cs.Epoch < s.staleEpoch && cs.Status != nagiosOk
})
}
// htmlHeader generates the HTML document header with embedded CSS and status summary.
func htmlHeader(subject string, numCriticals, numWarnings, numUnknown, numStale, numOK int) string {
var sb strings.Builder
sb.WriteString(`
`)
sb.WriteString(html.EscapeString(subject))
sb.WriteString(`
Gogios Status Report
C:`)
sb.WriteString(fmt.Sprintf("%d W:%d U:%d S:%d OK:%d", numCriticals, numWarnings, numUnknown, numStale, numOK))
sb.WriteString(`
Last Updated: `)
sb.WriteString(time.Now().Format("2006-01-02 15:04:05 MST"))
sb.WriteString(`
`)
return sb.String()
}
// htmlFooter generates the HTML document footer.
func htmlFooter() string {
var sb strings.Builder
sb.WriteString(`
`)
return sb.String()
}
// htmlStatusBadge generates a colored HTML span for a status code.
func htmlStatusBadge(status nagiosCode) string {
statusStr := status.Str()
return fmt.Sprintf(`%s`, statusStr, statusStr)
}