package webui import ( "encoding/json" "fmt" "net/http" "time" "bee/audit/internal/app" "bee/audit/internal/platform" ) func (h *handler) handleAPIMetricsLatest(w http.ResponseWriter, r *http.Request) { sample, ok := h.latestMetric() if !ok { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte("{}")) return } b, err := json.Marshal(sample) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(b) } func (h *handler) handleAPIMetricsStream(w http.ResponseWriter, r *http.Request) { if !sseStart(w) { return } ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-r.Context().Done(): return case <-ticker.C: sample, ok := h.latestMetric() if !ok { continue } b, err := json.Marshal(sample) if err != nil { continue } if !sseWrite(w, "metrics", string(b)) { return } } } } // feedRings pushes one sample into all in-memory ring buffers. func (h *handler) feedRings(sample platform.LiveMetricSample) { for _, t := range sample.Temps { switch t.Group { case "cpu": h.pushNamedMetricRing(&h.cpuTempRings, t.Name, t.Celsius) case "ambient": h.pushNamedMetricRing(&h.ambientTempRings, t.Name, t.Celsius) } } h.ringPower.push(sample.PowerW) h.ringCPULoad.push(sample.CPULoadPct) h.ringMemLoad.push(sample.MemLoadPct) h.ringsMu.Lock() h.pushFanRings(sample.Fans) for _, gpu := range sample.GPUs { idx := gpu.GPUIndex for len(h.gpuRings) <= idx { h.gpuRings = append(h.gpuRings, &gpuRings{ Temp: newMetricsRing(120), Util: newMetricsRing(120), MemUtil: newMetricsRing(120), Power: newMetricsRing(120), }) } h.gpuRings[idx].Temp.push(gpu.TempC) h.gpuRings[idx].Util.push(gpu.UsagePct) h.gpuRings[idx].MemUtil.push(gpu.MemUsagePct) h.gpuRings[idx].Power.push(gpu.PowerW) } h.ringsMu.Unlock() } func (h *handler) pushFanRings(fans []platform.FanReading) { if len(fans) == 0 && len(h.ringFans) == 0 { return } fanValues := make(map[string]float64, len(fans)) for _, fan := range fans { if fan.Name == "" { continue } fanValues[fan.Name] = fan.RPM found := false for i, name := range h.fanNames { if name == fan.Name { found = true if i >= len(h.ringFans) { h.ringFans = append(h.ringFans, newMetricsRing(120)) } break } } if !found { h.fanNames = append(h.fanNames, fan.Name) h.ringFans = append(h.ringFans, newMetricsRing(120)) } } for i, ring := range h.ringFans { if ring == nil { continue } name := "" if i < len(h.fanNames) { name = h.fanNames[i] } if rpm, ok := fanValues[name]; ok { ring.push(rpm) continue } if last, ok := ring.latest(); ok { ring.push(last) continue } ring.push(0) } } func (h *handler) pushNamedMetricRing(dst *[]*namedMetricsRing, name string, value float64) { if name == "" { return } for _, item := range *dst { if item != nil && item.Name == name && item.Ring != nil { item.Ring.push(value) return } } *dst = append(*dst, &namedMetricsRing{ Name: name, Ring: newMetricsRing(120), }) (*dst)[len(*dst)-1].Ring.push(value) } // ── Network toggle ──────────────────────────────────────────────────────────── const netRollbackTimeout = 60 * time.Second func (h *handler) handleAPINetworkToggle(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Iface string `json:"iface"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Iface == "" { writeError(w, http.StatusBadRequest, "iface is required") return } wasUp, err := h.opts.App.GetInterfaceState(req.Iface) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if _, err := h.applyPendingNetworkChange(func() (app.ActionResult, error) { err := h.opts.App.SetInterfaceState(req.Iface, !wasUp) return app.ActionResult{}, err }); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } newState := "up" if wasUp { newState = "down" } writeJSON(w, map[string]any{ "iface": req.Iface, "new_state": newState, "rollback_in": int(netRollbackTimeout.Seconds()), }) } func (h *handler) applyPendingNetworkChange(apply func() (app.ActionResult, error)) (app.ActionResult, error) { if h.opts.App == nil { return app.ActionResult{}, fmt.Errorf("app not configured") } if err := h.rollbackPendingNetworkChange(); err != nil && err.Error() != "no pending network change" { return app.ActionResult{}, err } snapshot, err := h.opts.App.CaptureNetworkSnapshot() if err != nil { return app.ActionResult{}, err } result, err := apply() if err != nil { return result, err } pnc := &pendingNetChange{ snapshot: snapshot, deadline: time.Now().Add(netRollbackTimeout), } pnc.timer = time.AfterFunc(netRollbackTimeout, func() { _ = h.opts.App.RestoreNetworkSnapshot(snapshot) h.pendingNetMu.Lock() if h.pendingNet == pnc { h.pendingNet = nil } h.pendingNetMu.Unlock() }) h.pendingNetMu.Lock() h.pendingNet = pnc h.pendingNetMu.Unlock() return result, nil } func (h *handler) hasPendingNetworkChange() bool { h.pendingNetMu.Lock() defer h.pendingNetMu.Unlock() return h.pendingNet != nil } func (h *handler) pendingNetworkRollbackIn() int { h.pendingNetMu.Lock() defer h.pendingNetMu.Unlock() if h.pendingNet == nil { return 0 } remaining := int(time.Until(h.pendingNet.deadline).Seconds()) if remaining < 1 { return 1 } return remaining } func (h *handler) handleAPINetworkConfirm(w http.ResponseWriter, _ *http.Request) { h.pendingNetMu.Lock() pnc := h.pendingNet h.pendingNet = nil h.pendingNetMu.Unlock() if pnc != nil { pnc.mu.Lock() pnc.timer.Stop() pnc.mu.Unlock() } writeJSON(w, map[string]string{"status": "confirmed"}) } func (h *handler) handleAPINetworkRollback(w http.ResponseWriter, _ *http.Request) { if err := h.rollbackPendingNetworkChange(); err != nil { if err.Error() == "no pending network change" { writeError(w, http.StatusConflict, err.Error()) return } writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, map[string]string{"status": "rolled back"}) } func (h *handler) handleAPIBenchmarkResults(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, renderBenchmarkResultsCard(h.opts.ExportDir)) } // ── Hardware summary / component detail ────────────────────────────────────── // handleAPIHardwareSummary returns the hardware summary card HTML fragment for // htmx polling (hx-get="/api/hardware-summary" hx-swap="outerHTML"). func (h *handler) handleAPIHardwareSummary(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") fmt.Fprint(w, renderHardwareSummaryCard(h.opts)) } // handleAPIComponentDetail returns an HTML fragment describing the current and // historical status for one component type (cpu, memory, storage, gpu, psu). func (h *handler) handleAPIComponentDetail(w http.ResponseWriter, r *http.Request) { compType := r.PathValue("type") var exact, prefixes []string var title string switch compType { case "cpu": title = "CPU" exact = []string{"cpu:all"} case "memory": title = "Memory" exact = []string{"memory:all"} prefixes = []string{"memory:"} case "storage": title = "Storage" exact = []string{"storage:all"} prefixes = []string{"storage:"} case "gpu": title = "GPU" prefixes = []string{"pcie:gpu:"} case "nic": title = "NIC" prefixes = []string{"pcie:nic:"} case "psu": title = "PSU" prefixes = []string{"psu:"} case "raid": title = "RAID" prefixes = []string{"pcie:raid:"} default: http.NotFound(w, r) return } var records []app.ComponentStatusRecord if h.opts.App != nil && h.opts.App.StatusDB != nil { all := h.opts.App.StatusDB.All() records = matchedRecords(all, exact, prefixes) } fromInventory := false if len(records) == 0 { if fallback := inventoryFallbackRecords(compType, h.opts); len(fallback) > 0 { records = fallback fromInventory = true } } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") fmt.Fprint(w, renderComponentDetail(title, records, fromInventory)) } func (h *handler) rollbackPendingNetworkChange() error { h.pendingNetMu.Lock() pnc := h.pendingNet h.pendingNet = nil h.pendingNetMu.Unlock() if pnc == nil { return fmt.Errorf("no pending network change") } pnc.mu.Lock() pnc.timer.Stop() pnc.mu.Unlock() if h.opts.App != nil { return h.opts.App.RestoreNetworkSnapshot(pnc.snapshot) } return nil }