Files
deploy-felhom-compose/controller/internal/web/funcmap.go
T
admin ded0cbb842 v0.6.1: Code review bugfixes — 7 correctness/safety/quality fixes
- Fix http.NotFound(w, nil) → pass actual request in handlers
- Fix dashboard running/stopped counts to match displayed stacks
- Fix Secure cookie blocking HTTP login (dynamic based on request)
- Remove misleading subtle.ConstantTimeCompare in session check
- Fix cleanupSessions goroutine leak (proper ticker + done channel)
- Add http.MaxBytesReader (1MB) to API POST endpoints
- Cache time.LoadLocation("Europe/Budapest") in template funcmap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 14:40:13 +01:00

290 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"fmt"
"html/template"
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// templateFuncMap returns the FuncMap used by all HTML templates.
func (s *Server) templateFuncMap() template.FuncMap {
loc, err := time.LoadLocation("Europe/Budapest")
if err != nil {
loc = time.UTC
}
return template.FuncMap{
"stateColor": func(state stacks.ContainerState) string {
switch state {
case stacks.StateRunning:
return "green"
case stacks.StateStarting:
return "orange"
case stacks.StateUnhealthy:
return "yellow"
case stacks.StateStopped, stacks.StateExited:
return "red"
case stacks.StateRestarting:
return "yellow"
default:
return "gray"
}
},
"stateLabel": func(state stacks.ContainerState) string {
switch state {
case stacks.StateRunning:
return "Fut"
case stacks.StateStarting:
return "Indulás..."
case stacks.StateUnhealthy:
return "Nem egészséges"
case stacks.StateStopped, stacks.StateExited:
return "Leállítva"
case stacks.StateRestarting:
return "Újraindítás..."
case stacks.StateNotDeployed:
return "Nincs telepítve"
case stacks.StatePaused:
return "Szüneteltetve"
default:
return "Ismeretlen"
}
},
"stateIcon": func(state stacks.ContainerState) string {
switch state {
case stacks.StateRunning:
return "●"
case stacks.StateStarting:
return "◐"
case stacks.StateUnhealthy:
return "◑"
case stacks.StateStopped, stacks.StateExited:
return "○"
case stacks.StateRestarting:
return "◐"
default:
return "◌"
}
},
"stateStr": func(state stacks.ContainerState) string {
return string(state)
},
// isOperational returns true for any state where the stack has containers
// and is not stopped/exited — used by templates for showing action buttons
"isOperational": func(state stacks.ContainerState) bool {
switch state {
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
return true
default:
return false
}
},
"logoURL": func(slug string) string {
return s.cfg.AppLogoURL(slug)
},
"logoPNGURL": func(slug string) string {
return s.cfg.AppLogoPNGURL(slug)
},
"appPageURL": func(slug string) string {
return s.cfg.AppPageURL(slug)
},
"usageColor": func(percent float64) string {
if percent >= 85 {
return "red"
}
if percent >= 70 {
return "yellow"
}
return "green"
},
"fmtMB": func(mb uint64) string {
if mb >= 1024 {
gb := float64(mb) / 1024.0
if gb >= 10 {
return fmt.Sprintf("%.0f GB", gb)
}
return fmt.Sprintf("%.1f GB", gb)
}
return fmt.Sprintf("%d MB", mb)
},
"fmtGB": func(gb float64) string {
if gb >= 100 {
return fmt.Sprintf("%.0f GB", gb)
}
if gb >= 10 {
return fmt.Sprintf("%.1f GB", gb)
}
return fmt.Sprintf("%.2f GB", gb)
},
"subtract": func(a, b int) int {
r := a - b
if r < 0 {
return 0
}
return r
},
"screenshotURL": func(slug string, index int) string {
return s.cfg.AppScreenshotURL(slug, index)
},
"seq": func(n int) []int {
result := make([]int, n)
for i := range result {
result[i] = i + 1
}
return result
},
"tempColor": func(celsius float64) string {
if celsius > 75 {
return "red"
}
if celsius >= 60 {
return "yellow"
}
return "green"
},
"fmtTemp": func(celsius float64) string {
return fmt.Sprintf("%.0f°C", celsius)
},
"fmtLoad": func(load float64) string {
return fmt.Sprintf("%.2f", load)
},
"filterCategory": func(state stacks.ContainerState, deployed bool) string {
switch state {
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
return "running"
case stacks.StateStopped, stacks.StateExited, stacks.StatePaused:
return "stopped"
default:
if deployed {
return "stopped"
}
return "available"
}
},
"timeAgo": func(t time.Time) string {
if t.IsZero() {
return ""
}
now := time.Now().In(loc)
d := now.Sub(t.In(loc))
switch {
case d < time.Minute:
return "most"
case d < time.Hour:
return fmt.Sprintf("%d perce", int(d.Minutes()))
case d < 24*time.Hour:
return fmt.Sprintf("%d órája", int(d.Hours()))
case d < 48*time.Hour:
return "tegnap"
default:
return fmt.Sprintf("%d napja", int(d.Hours()/24))
}
},
"fmtTime": func(t time.Time) string {
if t.IsZero() {
return ""
}
return t.In(loc).Format("2006-01-02 15:04")
},
"fmtTimeShort": func(t time.Time) string {
if t.IsZero() {
return ""
}
lt := t.In(loc)
now := time.Now().In(loc)
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
return lt.Format("15:04")
}
return lt.Format("01-02 15:04")
},
"dbTypeLabel": func(t backup.DBType) string {
switch t {
case backup.DBTypePostgres:
return "PostgreSQL"
case backup.DBTypeMariaDB:
return "MariaDB"
default:
return string(t)
}
},
"nextRunLabel": func(t time.Time) string {
if t.IsZero() {
return ""
}
lt := t.In(loc)
now := time.Now().In(loc)
timeStr := lt.Format("15:04")
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
return "ma " + timeStr
}
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay()+1 {
return "holnap " + timeStr
}
return lt.Format("2006-01-02") + " " + timeStr
},
"pruneLabel": func(s string) string {
switch strings.ToLower(s) {
case "weekly":
return "vasárnap"
case "daily":
return "naponta"
case "sunday":
return "vasárnap"
default:
return s
}
},
"nextPruneLabel": func(schedule string) string {
now := time.Now().In(loc)
var next time.Time
switch strings.ToLower(schedule) {
case "daily":
next = now.Add(24 * time.Hour)
default: // weekly/sunday
daysUntilSunday := (7 - int(now.Weekday())) % 7
if daysUntilSunday == 0 && now.Hour() >= 4 {
daysUntilSunday = 7
}
next = now.AddDate(0, 0, daysUntilSunday)
}
return next.Format("2006-01-02")
},
"fmtDuration": func(d time.Duration) string {
if d < time.Second {
return "< 1s"
}
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
return fmt.Sprintf("%dm %ds", int(d.Minutes()), int(d.Seconds())%60)
},
"fmtBytes": func(b int64) string {
const (
kb = 1024
mb = 1024 * kb
gb = 1024 * mb
)
switch {
case b >= int64(gb):
return fmt.Sprintf("%.1f GB", float64(b)/float64(gb))
case b >= int64(mb):
return fmt.Sprintf("%.1f MB", float64(b)/float64(mb))
case b >= int64(kb):
return fmt.Sprintf("%.1f KB", float64(b)/float64(kb))
default:
return fmt.Sprintf("%d B", b)
}
},
"shortID": func(id string) string {
if len(id) > 8 {
return id[:8]
}
return id
},
}
}