package webui import ( "context" "encoding/json" "fmt" "net/http" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "time" "bee/audit/internal/platform" ) func (h *handler) handleAPIGNVIDIAGPUs(w http.ResponseWriter, _ *http.Request) { writeAPIListResponse(w, h.opts.App != nil, func() ([]platform.NvidiaGPU, error) { return h.opts.App.ListNvidiaGPUs() }) } func (h *handler) handleAPIGNVIDIAGPUStatuses(w http.ResponseWriter, _ *http.Request) { writeAPIListResponse(w, h.opts.App != nil, func() ([]platform.NvidiaGPUStatus, error) { return apiListNvidiaGPUStatuses(h.opts.App) }) } func (h *handler) handleAPIGNVIDIAReset(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Index int `json:"index"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } result, err := h.opts.App.ResetNvidiaGPU(req.Index) status := "ok" if err != nil { status = "error" } writeJSON(w, map[string]string{"status": status, "output": result.Body}) } // ── GPU settings (ECC / power limit) ────────────────────────────────────────── func (h *handler) handleAPIGNVIDIAGPUSettings(w http.ResponseWriter, _ *http.Request) { writeAPIListResponse(w, h.opts.App != nil, func() ([]platform.NvidiaGPUSetting, error) { return h.opts.App.ListNvidiaGPUSettings() }) } func (h *handler) handleAPIGNVIDIASetECC(w http.ResponseWriter, r *http.Request) { h.handleAPIGNVIDIASetBool(w, r, func(index int, enabled bool) (string, error) { result, err := h.opts.App.SetNvidiaGPUECC(index, enabled) return result.Body, err }) } func (h *handler) handleAPIGNVIDIASetMIG(w http.ResponseWriter, r *http.Request) { h.handleAPIGNVIDIASetBool(w, r, func(index int, enabled bool) (string, error) { result, err := h.opts.App.SetNvidiaGPUMIG(index, enabled) return result.Body, err }) } func (h *handler) handleAPIGNVIDIASetCCMode(w http.ResponseWriter, r *http.Request) { h.handleAPIGNVIDIASetBool(w, r, func(index int, enabled bool) (string, error) { result, err := h.opts.App.SetNvidiaGPUCCMode(index, enabled) return result.Body, err }) } func (h *handler) handleAPIGNVIDIASetBool(w http.ResponseWriter, r *http.Request, apply func(int, bool) (string, error)) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Index int `json:"index"` Enabled bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } output, err := apply(req.Index, req.Enabled) status := "ok" if err != nil { status = "error" } writeJSON(w, map[string]string{"status": status, "output": output}) } func (h *handler) handleAPIGNVIDIASetPowerLimit(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Index int `json:"index"` Watts float64 `json:"watts"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Watts <= 0 { writeError(w, http.StatusBadRequest, "watts must be > 0") return } result, err := h.opts.App.SetNvidiaGPUPowerLimit(req.Index, req.Watts) status := "ok" if err != nil { status = "error" } writeJSON(w, map[string]string{"status": status, "output": result.Body}) } func (h *handler) handleAPIGNVIDIAResetDefaults(w http.ResponseWriter, _ *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } result, err := h.opts.App.ResetNvidiaGPUDefaults() status := "ok" if err != nil { status = "error" } writeJSON(w, map[string]string{"status": status, "output": result.Body}) } func (h *handler) handleAPIGPUPresence(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } gp := h.opts.App.DetectGPUPresence() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]bool{ "nvidia": gp.Nvidia, "amd": gp.AMD, "nvidia_initializing": gp.NvidiaInitializing, "amd_initializing": gp.AMDInitializing, }) } // ── GPU tools ───────────────────────────────────────────────────────────────── func (h *handler) handleAPIGPUTools(w http.ResponseWriter, _ *http.Request) { type toolEntry struct { ID string `json:"id"` Available bool `json:"available"` Vendor string `json:"vendor"` // "nvidia" | "amd" } // Single source of truth for GPU presence: see app.DetectGPUPresence. var nvidiaUp, amdUp bool if h.opts.App != nil { gp := h.opts.App.DetectGPUPresence() nvidiaUp, amdUp = gp.Nvidia, gp.AMD } else { _, nvidiaErr := os.Stat("/dev/nvidia0") _, amdErr := os.Stat("/dev/kfd") nvidiaUp, amdUp = nvidiaErr == nil, amdErr == nil } _, dcgmErr := exec.LookPath("dcgmi") _, ncclStressErr := exec.LookPath("bee-nccl-gpu-stress") _, johnErr := exec.LookPath("bee-john-gpu-stress") _, beeBurnErr := exec.LookPath("bee-gpu-burn") _, nvBandwidthErr := exec.LookPath("nvbandwidth") profErr := lookPathAny("dcgmproftester", "dcgmproftester13", "dcgmproftester12", "dcgmproftester11") writeJSON(w, []toolEntry{ {ID: "nvidia-compute", Available: nvidiaUp && profErr == nil, Vendor: "nvidia"}, {ID: "nvidia-targeted-power", Available: nvidiaUp && dcgmErr == nil, Vendor: "nvidia"}, {ID: "nvidia-pulse", Available: nvidiaUp && dcgmErr == nil, Vendor: "nvidia"}, {ID: "nvidia-interconnect", Available: nvidiaUp && ncclStressErr == nil, Vendor: "nvidia"}, {ID: "nvidia-bandwidth", Available: nvidiaUp && dcgmErr == nil && nvBandwidthErr == nil, Vendor: "nvidia"}, {ID: "bee-gpu-burn", Available: nvidiaUp && beeBurnErr == nil, Vendor: "nvidia"}, {ID: "john", Available: nvidiaUp && johnErr == nil, Vendor: "nvidia"}, {ID: "rvs", Available: amdUp, Vendor: "amd"}, }) } func lookPathAny(names ...string) error { for _, name := range names { if _, err := exec.LookPath(name); err == nil { return nil } } return exec.ErrNotFound } // ── System ──────────────────────────────────────────────────────────────────── func (h *handler) handleAPIRAMStatus(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } status := h.currentRAMStatus() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(status) } type ramStatusResponse struct { platform.LiveMediaRAMState InstallTaskActive bool `json:"install_task_active,omitempty"` CopyTaskActive bool `json:"copy_task_active,omitempty"` CanStartTask bool `json:"can_start_task,omitempty"` BlockedReason string `json:"blocked_reason,omitempty"` } func (h *handler) currentRAMStatus() ramStatusResponse { state := h.opts.App.LiveMediaRAMState() resp := ramStatusResponse{LiveMediaRAMState: state} if globalQueue.hasActiveTarget("install") { resp.InstallTaskActive = true resp.BlockedReason = "install to disk is already running" return resp } if globalQueue.hasActiveTarget("install-to-ram") { resp.CopyTaskActive = true resp.BlockedReason = "install to RAM task is already pending or running" return resp } if state.InRAM { resp.BlockedReason = "system is already running from RAM" return resp } resp.CanStartTask = state.CanStartCopy if !resp.CanStartTask && resp.BlockedReason == "" { resp.BlockedReason = state.Message } return resp } func (h *handler) handleAPIInstallToRAM(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } status := h.currentRAMStatus() if !status.CanStartTask { msg := strings.TrimSpace(status.BlockedReason) if msg == "" { msg = "install to RAM is not available" } writeError(w, http.StatusConflict, msg) return } t := &Task{ ID: newJobID("install-to-ram"), Name: "Install to RAM", Target: "install-to-ram", Priority: defaultTaskPriority("install-to-ram", taskParams{}), Status: TaskPending, CreatedAt: time.Now(), } globalQueue.enqueue(t) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"task_id": t.ID}) } func (h *handler) handleAPISystemReboot(w http.ResponseWriter, r *http.Request) { if err := exec.Command("systemctl", "reboot").Start(); err != nil { writeError(w, http.StatusInternalServerError, "reboot failed: "+err.Error()) return } writeJSON(w, map[string]string{"status": "rebooting"}) } func (h *handler) handleAPISystemShutdown(w http.ResponseWriter, r *http.Request) { if err := exec.Command("systemctl", "poweroff").Start(); err != nil { writeError(w, http.StatusInternalServerError, "shutdown failed: "+err.Error()) return } writeJSON(w, map[string]string{"status": "shutting down"}) } // timezoneNameRE matches IANA timezone identifiers like "Europe/Moscow" or "UTC". var timezoneNameRE = regexp.MustCompile(`^[A-Za-z0-9_+\-]+(/[A-Za-z0-9_+\-]+)*$`) func validTimezoneName(tz string) bool { if tz == "" || !timezoneNameRE.MatchString(tz) { return false } _, err := os.Stat(filepath.Join("/usr/share/zoneinfo", tz)) return err == nil } // handleAPISystemTimeSync sets the host's timezone and wall-clock time from // values supplied by the client's browser (used when the appliance has no // network/NTP access to keep its own clock in sync). func (h *handler) handleAPISystemTimeSync(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() var req struct { Timezone string `json:"timezone"` EpochMS int64 `json:"epoch_ms"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.EpochMS <= 0 { writeError(w, http.StatusBadRequest, "epoch_ms required") return } var out strings.Builder if req.Timezone != "" { if !validTimezoneName(req.Timezone) { writeError(w, http.StatusBadRequest, "invalid timezone") return } if b, err := exec.CommandContext(ctx, "timedatectl", "set-timezone", req.Timezone).CombinedOutput(); err != nil { writeError(w, http.StatusInternalServerError, "set-timezone failed: "+commandFailureDetail(b, err)) return } fmt.Fprintf(&out, "timezone set to %s\n", req.Timezone) } // Manual time only sticks if NTP sync is off. if b, err := exec.CommandContext(ctx, "timedatectl", "set-ntp", "false").CombinedOutput(); err != nil { canNTP, canErr := exec.CommandContext(ctx, "timedatectl", "show", "-p", "CanNTP", "--value").Output() if canErr != nil || strings.TrimSpace(string(canNTP)) != "no" { writeError(w, http.StatusInternalServerError, "disable NTP failed: "+commandFailureDetail(b, err)) return } } sec := req.EpochMS / 1000 if b, err := exec.CommandContext(ctx, "date", "-u", "-s", "@"+strconv.FormatInt(sec, 10)).CombinedOutput(); err != nil { writeError(w, http.StatusInternalServerError, "set-time failed: "+commandFailureDetail(b, err)) return } out.WriteString("system clock synced\n") writeJSON(w, map[string]string{"status": "ok", "output": out.String()}) } func commandFailureDetail(output []byte, err error) string { if detail := strings.TrimSpace(string(output)); detail != "" { return detail } return err.Error() } // handleAPISystemTime reports the host's current wall-clock time and configured // timezone so the dashboard can show them next to the browser's own clock and // flag a drift. func (h *handler) handleAPISystemTime(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() tz := "" if b, err := exec.CommandContext(ctx, "timedatectl", "show", "-p", "Timezone", "--value").Output(); err == nil { tz = strings.TrimSpace(string(b)) } now := time.Now() if tz == "" { tz, _ = now.Zone() } writeJSON(w, map[string]any{ "epoch_ms": now.UnixMilli(), "local_time": now.Format("2006-01-02 15:04:05"), "timezone": tz, }) } // ── Tools ───────────────────────────────────────────────────────────────────── var standardTools = []string{ "dmidecode", "smartctl", "nvme", "lspci", "ipmitool", "tpm2_getcap", "tpm2_pcrread", "tpm2_gettestresult", "nvidia-smi", "dcgmi", "nv-hostengine", "memtester", "stress-ng", "nvtop", "mstflint", "saa", } func (h *handler) handleAPIToolsCheck(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } statuses := h.opts.App.CheckTools(standardTools) writeJSON(w, statuses) } // ── Preflight ───────────────────────────────────────────────────────────────── func (h *handler) handleAPIPreflight(w http.ResponseWriter, r *http.Request) { data, err := loadSnapshot(filepath.Join(h.opts.ExportDir, "runtime-health.json")) if err != nil { writeError(w, http.StatusNotFound, "runtime health not found") return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = w.Write(data) } // ── Install ─────────────────────────────────────────────────────────────────── func (h *handler) handleAPIInstallDisks(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } disks, err := h.opts.App.ListInstallDisks() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } type diskJSON struct { Device string `json:"device"` Model string `json:"model"` Size string `json:"size"` SizeBytes int64 `json:"size_bytes"` MountedParts []string `json:"mounted_parts"` Warnings []string `json:"warnings"` } result := make([]diskJSON, 0, len(disks)) for _, d := range disks { result = append(result, diskJSON{ Device: d.Device, Model: d.Model, Size: d.Size, SizeBytes: d.SizeBytes, MountedParts: d.MountedParts, Warnings: platform.DiskWarnings(d), }) } writeJSON(w, result) } func (h *handler) handleAPIInstallRun(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Device string `json:"device"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Device == "" { writeError(w, http.StatusBadRequest, "device is required") return } // Whitelist: only allow devices that ListInstallDisks() returns. disks, err := h.opts.App.ListInstallDisks() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } allowed := false for _, d := range disks { if d.Device == req.Device { allowed = true break } } if !allowed { writeError(w, http.StatusBadRequest, "device not in install candidate list") return } if globalQueue.hasActiveTarget("install-to-ram") { writeError(w, http.StatusConflict, "install to RAM task is already pending or running") return } if globalQueue.hasActiveTarget("install") { writeError(w, http.StatusConflict, "install task is already pending or running") return } t := &Task{ ID: newJobID("install"), Name: "Install to Disk", Target: "install", Priority: defaultTaskPriority("install", taskParams{}), Status: TaskPending, CreatedAt: time.Now(), params: taskParams{ Device: req.Device, }, } globalQueue.enqueue(t) writeJSON(w, map[string]string{"task_id": t.ID, "job_id": t.ID}) } // ── Metrics SSE ───────────────────────────────────────────────────────────────