ded0cbb842
- 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>
290 lines
6.9 KiB
Go
290 lines
6.9 KiB
Go
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
|
||
},
|
||
}
|
||
}
|