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, "" } }