13c6a0929a
Part A of the UI-fixes/storage-spike spec.
A1: enrichHostStorageTargets sorts /api/host-metrics storage_targets
server-side and attaches friendly Hungarian labels + purpose, fixing the
#host-storage-bars reorder-on-poll bug. Display labels only — PVE storage
ids are never renamed.
A2: new GET/POST /stacks/{name}/backup Tier-2 config panel; the "2. mentés"
Beállítás button is repointed there from the dead-end deploy page. Customer
can pin a target drive or disable Tier 2; preference is preserved across the
runner's status writes. Always visible (single-SSD + non-HDD apps included).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
4.3 KiB
Go
104 lines
4.3 KiB
Go
package web
|
||
|
||
import (
|
||
"net/http"
|
||
"sort"
|
||
|
||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||
)
|
||
|
||
// Agent-backed host metrics (slice 9).
|
||
//
|
||
// The de-privileged controller (slice 8C) sees only its own cgroup, so it cannot read host
|
||
// health itself. This thin proxy forwards GET /api/host-metrics to the agent's GET /host/metrics
|
||
// and returns the host-wide view (cpu%/mem/load/uptime/cpu-temp + per-storage capacity) for the
|
||
// monitoring page. It reuses the same pinned agentapi.Client + {ok,data,error} envelope as the
|
||
// disk proxy (agent_disk_handlers.go). Read-only; no CSRF mutation.
|
||
|
||
// ServeHostMetricsAPI proxies GET /api/host-metrics → agent GET /host/metrics.
|
||
// Wired in main.go behind RequireAuth.
|
||
func (s *Server) ServeHostMetricsAPI(w http.ResponseWriter, r *http.Request) {
|
||
if s.isDebug() {
|
||
s.logger.Printf("[DEBUG] [web] ServeHostMetricsAPI: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||
}
|
||
if r.Method != http.MethodGet {
|
||
writeDiskJSON(w, http.StatusMethodNotAllowed, false, "method not allowed", nil)
|
||
return
|
||
}
|
||
client, err := s.agentClient()
|
||
if err != nil {
|
||
// Unprovisioned guest / no local API configured — the UI shows "host metrics unavailable".
|
||
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
|
||
return
|
||
}
|
||
resp, err := client.HostMetrics(r.Context())
|
||
if err != nil {
|
||
s.logger.Printf("[ERROR] [web] host metrics via agent failed: %v", err)
|
||
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
||
return
|
||
}
|
||
// The agent enumerates storages via `pvesm` in a non-deterministic order, so #host-storage-bars
|
||
// reordered on every poll (item 2). Stabilise the order Go-side and attach friendly Hungarian
|
||
// labels + a one-line purpose per entry — display-only; we NEVER rename the PVE storage ids.
|
||
enrichHostStorageTargets(resp.StorageTargets)
|
||
writeDiskJSON(w, http.StatusOK, true, "", resp)
|
||
}
|
||
|
||
// enrichHostStorageTargets sorts the host's storage targets into a stable, customer-meaningful
|
||
// order (user-data → system+apps → backup → other; alphabetical by id within a tier) and fills in
|
||
// a friendly label + purpose per entry. Mirrors the disk-overview's sortDisksForView contract:
|
||
// a Go-side ordering beats relying on the agent's enumeration order or template JS.
|
||
func enrichHostStorageTargets(targets []agentapi.StorageTarget) {
|
||
sort.SliceStable(targets, func(i, j int) bool {
|
||
if ri, rj := storageTypeRank(targets[i].Type), storageTypeRank(targets[j].Type); ri != rj {
|
||
return ri < rj
|
||
}
|
||
return targets[i].Name < targets[j].Name
|
||
})
|
||
for i := range targets {
|
||
label, purpose := storageLabelAndPurpose(targets[i])
|
||
targets[i].Label = label
|
||
targets[i].Purpose = purpose
|
||
}
|
||
}
|
||
|
||
// storageTypeRank orders storage by what the customer cares about: where their app data lives
|
||
// first, then the system/app disk, then backup targets. Lower sorts first.
|
||
func storageTypeRank(typ string) int {
|
||
switch typ {
|
||
case "usb", "local-dir":
|
||
return 0 // external user-data drives (where browsable app data lives)
|
||
case "lvmthin", "lvm":
|
||
return 1 // the internal SSD: OS + the guest/app volumes
|
||
case "local":
|
||
return 2 // builtin dir: templates + local vzdump backups
|
||
case "pbs", "nfs", "cifs":
|
||
return 3 // backup targets (offsite / network)
|
||
default:
|
||
return 4
|
||
}
|
||
}
|
||
|
||
// storageLabelAndPurpose maps a storage target to a friendly Hungarian label + one-line purpose.
|
||
// Falls back to the raw id for unrecognised types. The raw id stays in Name (rendered muted).
|
||
func storageLabelAndPurpose(t agentapi.StorageTarget) (string, string) {
|
||
switch t.Type {
|
||
case "usb":
|
||
return "Külső adattároló (USB)", "Az alkalmazások adatai (fájlok, médiatár) ezen a meghajtón vannak."
|
||
case "local-dir":
|
||
return "Külső adattároló", "Az alkalmazások adatai (fájlok, médiatár) ezen a meghajtón vannak."
|
||
case "lvmthin", "lvm":
|
||
return "Belső SSD – rendszer és alkalmazások", "Az operációs rendszer és a telepített alkalmazások tárhelye."
|
||
case "local":
|
||
return "Belső lemez – sablonok és helyi mentések", "Rendszersablonok és helyi biztonsági mentések."
|
||
case "pbs":
|
||
return "Távoli biztonsági mentés", "Titkosított, telephelyen kívüli biztonsági mentések."
|
||
case "nfs":
|
||
return "Hálózati mentés (NFS)", "Hálózati tárolón őrzött biztonsági mentések."
|
||
case "cifs":
|
||
return "Hálózati mentés (SMB)", "Hálózati tárolón őrzött biztonsági mentések."
|
||
default:
|
||
return t.Name, ""
|
||
}
|
||
}
|