package webapp import ( "fmt" "strings" "time" ) // Template helpers. Kept in their own file so templates.go stays // focused on parsing/wiring. // fmtBytes renders a byte count as a human-readable string. The // unit is picked automatically (B / KiB / MiB / GiB / TiB). func fmtBytes(n uint64) string { const k = 1024 if n < k { return fmt.Sprintf("%d B", n) } div, exp := uint64(k), 1 for n2 := n / k; n2 >= k; n2 /= k { div *= k exp++ } suffix := []string{"KiB", "MiB", "GiB", "TiB", "PiB"}[exp-1] return fmt.Sprintf("%.2f %s", float64(n)/float64(div), suffix) } // fmtPercent renders a 0..100 number as "N.NN%". func fmtPercent(p float64) string { return fmt.Sprintf("%.2f%%", p) } // fmtDuration renders a duration as "1d 2h 3m 4s". The format is // stable across short and long uptimes. func fmtDuration(d time.Duration) string { if d < 0 { d = 0 } days := int(d / (24 * time.Hour)) d -= time.Duration(days) * 24 * time.Hour hours := int(d / time.Hour) d -= time.Duration(hours) * time.Hour minutes := int(d / time.Minute) d -= time.Duration(minutes) * time.Minute seconds := int(d / time.Second) switch { case days > 0: return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds) case hours > 0: return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds) case minutes > 0: return fmt.Sprintf("%dm %ds", minutes, seconds) default: return fmt.Sprintf("%ds", seconds) } } // fmtTime renders a timestamp as "2006-01-02 15:04:05 UTC". func fmtTime(t time.Time) string { if t.IsZero() { return "—" } return t.UTC().Format("2006-01-02 15:04:05 UTC") } // fmtBool renders a bool as "yes" / "no". func fmtBool(b bool) string { if b { return "yes" } return "no" } // sanitizeName strips characters that could break out of an HTML // attribute when interpolated by a template. Template auto-escapes // by default, but a paranoid layer here keeps audit targets safe // when they are rendered into other contexts. func sanitizeName(s string) string { s = strings.ReplaceAll(s, "\x00", "") if len(s) > 128 { s = s[:128] } return s }