Files
deploy-felhom-compose/controller/internal/web/handlers.go
T
admin d32d9fb44b v0.4.0: monitoring & backup — scheduler, CPU/temp metrics, healthchecks, restic backups
Phase 2 (Monitoring & Health):
- Central job scheduler replacing ad-hoc goroutines (internal/scheduler)
- CPU usage collector via /proc/stat background sampling (internal/system/cpu_linux.go)
- Temperature reading from /sys/class/thermal + /host/sys (Docker mount)
- Load average from /proc/loadavg
- Healthchecks.io-compatible HTTP pinger (internal/monitor/pinger.go)
- System health checks: disk, memory, CPU, temp, Docker, protected containers (internal/monitor/healthcheck.go)

Phase 3 (Backups):
- Database auto-discovery via docker ps + docker inspect (internal/backup/dbdump.go)
- Database dumping via docker exec (pg_dump / mariadb-dump) with atomic writes
- Restic backup integration with auto-password generation (internal/backup/restic.go)
- Backup orchestrator: DB dumps + restic snapshots + weekly prune (internal/backup/backup.go)
- Manual backup trigger via dashboard button and POST /api/backup/run

Dashboard UI:
- CPU usage bar with load average display
- Temperature with colored indicator dot
- Backup status card with last run time, DB count, repo stats
- "Mentés most" button for manual backup trigger

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 11:17:10 +01:00

184 lines
5.2 KiB
Go

package web
import (
"fmt"
"net/http"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
)
func (s *Server) baseData(page, title string) map[string]interface{} {
return map[string]interface{}{
"Page": page,
"Title": title,
"CustomerName": s.cfg.Customer.Name,
"Domain": s.cfg.Customer.Domain,
"Version": s.version,
}
}
func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) {
stackList := s.stackMgr.GetStacks()
running, stopped := 0, 0
for _, st := range stackList {
switch st.State {
case stacks.StateRunning:
running++
case stacks.StateStopped, stacks.StateExited:
stopped++
case stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
// Count starting/unhealthy/restarting as "running" for the dashboard stat
// (they have containers, they're just not fully healthy yet)
running++
}
}
sysInfo := system.GetInfo(s.cfg.Paths.HDDPath, s.cpuCollector)
data := s.baseData("dashboard", "Vezérlőpult")
data["Stacks"] = stackList
data["RunningCount"] = running
data["StoppedCount"] = stopped
data["TotalCount"] = len(stackList)
data["SystemInfo"] = sysInfo
// Backup status
data["BackupEnabled"] = s.cfg.Backup.Enabled
if s.backupMgr != nil {
dbDump, backupSt := s.backupMgr.GetStatus()
data["DBDumpStatus"] = dbDump
data["BackupStatus"] = backupSt
data["BackupRunning"] = s.backupMgr.IsRunning()
data["BackupMaxAgeHours"] = s.cfg.Monitoring.Thresholds.BackupMaxAgeHours
}
s.render(w, "dashboard", data)
}
func (s *Server) stacksHandler(w http.ResponseWriter, _ *http.Request) {
data := s.baseData("stacks", "Alkalmazások")
data["Stacks"] = s.stackMgr.GetStacks()
s.render(w, "stacks", data)
}
func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string) {
stack, ok := s.stackMgr.GetStack(name)
if !ok {
http.NotFound(w, r)
return
}
logs, err := s.stackMgr.GetLogs(name, 200)
if err != nil {
logs = fmt.Sprintf("Hiba a naplók lekérésekor: %v", err)
}
// Raw mode: return plain text for AJAX polling
if r.URL.Query().Get("raw") == "1" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, logs)
return
}
data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók")
data["Stack"] = stack
data["Logs"] = logs
s.render(w, "logs", data)
}
func (s *Server) deployHandler(w http.ResponseWriter, _ *http.Request, name string) {
meta, appCfg, err := s.stackMgr.GetDeployFields(name)
if err != nil {
http.NotFound(w, nil)
return
}
stack, _ := s.stackMgr.GetStack(name)
alreadyDeployed := appCfg != nil && appCfg.Deployed
data := s.baseData("deploy", meta.DisplayName+" — Telepítés")
data["Stack"] = stack
data["Meta"] = meta
data["AppConfig"] = appCfg
data["AlreadyDeployed"] = alreadyDeployed
data["LogoURL"] = s.cfg.AppLogoURL(meta.Slug)
data["LogoPNGURL"] = s.cfg.AppLogoPNGURL(meta.Slug)
data["AppPageURL"] = s.cfg.AppPageURL(meta.Slug)
data["UserFields"] = meta.UserFacingFields()
data["AutoFields"] = meta.AutoGeneratedFields()
// Memory info for deploy page (only for non-deployed apps)
if !alreadyDeployed {
memInfo := map[string]interface{}{"Available": false}
totalMB, memErr := system.GetTotalMemoryMB()
if memErr == nil {
reservedMB := s.cfg.System.ReservedMemoryMB
usableMB := totalMB - reservedMB
committedReqMB, committedLimitMB := s.stackMgr.CommittedMemory()
newReqMB := stacks.ParseMemoryMB(meta.Resources.MemRequest)
newLimitMB := stacks.ParseMemoryMB(meta.Resources.MemLimit)
afterReqMB := committedReqMB + newReqMB
afterLimitMB := committedLimitMB + newLimitMB
percent := 0
if usableMB > 0 {
percent = afterReqMB * 100 / usableMB
}
committedPercent := 0
if usableMB > 0 {
committedPercent = committedReqMB * 100 / usableMB
}
memInfo["Available"] = true
memInfo["TotalMB"] = totalMB
memInfo["ReservedMB"] = reservedMB
memInfo["UsableMB"] = usableMB
memInfo["CommittedMB"] = committedReqMB
memInfo["NewRequestMB"] = newReqMB
memInfo["AfterMB"] = afterReqMB
memInfo["Percent"] = percent
memInfo["CommittedPercent"] = committedPercent
memInfo["Blocked"] = newReqMB > 0 && afterReqMB > usableMB
memInfo["OvercommitWarn"] = newLimitMB > 0 && afterLimitMB > totalMB
}
data["MemoryInfo"] = memInfo
}
s.render(w, "deploy", data)
}
func (s *Server) appDetailHandler(w http.ResponseWriter, _ *http.Request, slug string) {
var found *stacks.Stack
for _, stack := range s.stackMgr.GetStacks() {
if stack.Meta.Slug == slug {
found = &stack
break
}
}
if found == nil {
http.NotFound(w, nil)
return
}
// Load current optional config values from app.yaml
currentValues := make(map[string]string)
if appCfg := s.stackMgr.LoadAppConfigByName(found.Name); appCfg != nil {
for k, v := range appCfg.Env {
currentValues[k] = v
}
}
data := s.baseData("stacks", found.Meta.DisplayName)
data["Stack"] = found
data["Meta"] = found.Meta
data["AppInfo"] = found.Meta.AppInfo
data["OptionalConfig"] = found.Meta.OptionalConfig
data["CurrentValues"] = currentValues
data["HasAppInfo"] = found.Meta.HasAppInfo()
data["HasOptionalConfig"] = found.Meta.HasOptionalConfig()
s.render(w, "app_info", data)
}