package webui import ( "bufio" "encoding/json" "errors" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" "sync/atomic" "syscall" "time" "bee/audit/internal/app" "bee/audit/internal/platform" ) var ansiEscapeRE = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b[()][A-Z0-9]|\x1b[DABC]`) var apiListNvidiaGPUs = func(a *app.App) ([]platform.NvidiaGPU, error) { if a == nil { return nil, fmt.Errorf("app not configured") } return a.ListNvidiaGPUs() } // ── Job ID counter ──────────────────────────────────────────────────────────── var jobCounter atomic.Uint64 func newJobID(_ string) string { start := int((jobCounter.Add(1) - 1) % 1000) globalQueue.mu.Lock() defer globalQueue.mu.Unlock() for offset := 0; offset < 1000; offset++ { n := (start + offset) % 1000 id := fmt.Sprintf("TASK-%03d", n) if !taskIDInUseLocked(id) { return id } } return fmt.Sprintf("TASK-%03d", start) } func taskIDInUseLocked(id string) bool { for _, t := range globalQueue.tasks { if t != nil && t.ID == id { return true } } return false } type taskRunResponse struct { TaskID string `json:"task_id,omitempty"` JobID string `json:"job_id,omitempty"` TaskIDs []string `json:"task_ids,omitempty"` JobIDs []string `json:"job_ids,omitempty"` TaskCount int `json:"task_count,omitempty"` } type nvidiaTaskSelection struct { GPUIndices []int Label string } func writeTaskRunResponse(w http.ResponseWriter, tasks []*Task) { if len(tasks) == 0 { writeJSON(w, taskRunResponse{}) return } ids := make([]string, 0, len(tasks)) for _, t := range tasks { if t == nil || strings.TrimSpace(t.ID) == "" { continue } ids = append(ids, t.ID) } resp := taskRunResponse{TaskCount: len(ids)} if len(ids) > 0 { resp.TaskID = ids[0] resp.JobID = ids[0] resp.TaskIDs = ids resp.JobIDs = ids } writeJSON(w, resp) } func shouldSplitHomogeneousNvidiaTarget(target string) bool { switch strings.TrimSpace(target) { case "nvidia", "nvidia-targeted-stress", "nvidia-benchmark", "nvidia-compute", "nvidia-targeted-power", "nvidia-pulse", "nvidia-interconnect", "nvidia-bandwidth", "nvidia-stress": return true default: return false } } func expandHomogeneousNvidiaSelections(gpus []platform.NvidiaGPU, include, exclude []int) ([]nvidiaTaskSelection, error) { if len(gpus) == 0 { return nil, fmt.Errorf("no NVIDIA GPUs detected") } indexed := make(map[int]platform.NvidiaGPU, len(gpus)) allIndices := make([]int, 0, len(gpus)) for _, gpu := range gpus { indexed[gpu.Index] = gpu allIndices = append(allIndices, gpu.Index) } sort.Ints(allIndices) selected := allIndices if len(include) > 0 { selected = make([]int, 0, len(include)) seen := make(map[int]struct{}, len(include)) for _, idx := range include { if _, ok := indexed[idx]; !ok { continue } if _, dup := seen[idx]; dup { continue } seen[idx] = struct{}{} selected = append(selected, idx) } sort.Ints(selected) } if len(exclude) > 0 { skip := make(map[int]struct{}, len(exclude)) for _, idx := range exclude { skip[idx] = struct{}{} } filtered := selected[:0] for _, idx := range selected { if _, ok := skip[idx]; ok { continue } filtered = append(filtered, idx) } selected = filtered } if len(selected) == 0 { return nil, fmt.Errorf("no NVIDIA GPUs selected") } modelGroups := make(map[string][]platform.NvidiaGPU) modelOrder := make([]string, 0) for _, idx := range selected { gpu := indexed[idx] model := strings.TrimSpace(gpu.Name) if model == "" { model = fmt.Sprintf("GPU %d", gpu.Index) } if _, ok := modelGroups[model]; !ok { modelOrder = append(modelOrder, model) } modelGroups[model] = append(modelGroups[model], gpu) } sort.Slice(modelOrder, func(i, j int) bool { left := modelGroups[modelOrder[i]] right := modelGroups[modelOrder[j]] if len(left) == 0 || len(right) == 0 { return modelOrder[i] < modelOrder[j] } return left[0].Index < right[0].Index }) var groups []nvidiaTaskSelection var singles []nvidiaTaskSelection for _, model := range modelOrder { group := modelGroups[model] sort.Slice(group, func(i, j int) bool { return group[i].Index < group[j].Index }) indices := make([]int, 0, len(group)) for _, gpu := range group { indices = append(indices, gpu.Index) } if len(indices) >= 2 { groups = append(groups, nvidiaTaskSelection{ GPUIndices: indices, Label: fmt.Sprintf("%s; GPUs %s", model, joinTaskIndices(indices)), }) continue } gpu := group[0] singles = append(singles, nvidiaTaskSelection{ GPUIndices: []int{gpu.Index}, Label: fmt.Sprintf("GPU %d — %s", gpu.Index, model), }) } return append(groups, singles...), nil } func joinTaskIndices(indices []int) string { parts := make([]string, 0, len(indices)) for _, idx := range indices { parts = append(parts, fmt.Sprintf("%d", idx)) } return strings.Join(parts, ",") } func formatSplitTaskName(baseName, selectionLabel string) string { baseName = strings.TrimSpace(baseName) selectionLabel = strings.TrimSpace(selectionLabel) if baseName == "" { return selectionLabel } if selectionLabel == "" { return baseName } return baseName + " (" + selectionLabel + ")" } func buildNvidiaTaskSet(target string, priority int, createdAt time.Time, params taskParams, baseName string, appRef *app.App, idPrefix string) ([]*Task, error) { if !shouldSplitHomogeneousNvidiaTarget(target) { t := &Task{ ID: newJobID(idPrefix), Name: baseName, Target: target, Priority: priority, Status: TaskPending, CreatedAt: createdAt, params: params, } return []*Task{t}, nil } gpus, err := apiListNvidiaGPUs(appRef) if err != nil { return nil, err } selections, err := expandHomogeneousNvidiaSelections(gpus, params.GPUIndices, params.ExcludeGPUIndices) if err != nil { return nil, err } tasks := make([]*Task, 0, len(selections)) for _, selection := range selections { taskParamsCopy := params taskParamsCopy.GPUIndices = append([]int(nil), selection.GPUIndices...) taskParamsCopy.ExcludeGPUIndices = nil displayName := formatSplitTaskName(baseName, selection.Label) taskParamsCopy.DisplayName = displayName tasks = append(tasks, &Task{ ID: newJobID(idPrefix), Name: displayName, Target: target, Priority: priority, Status: TaskPending, CreatedAt: createdAt, params: taskParamsCopy, }) } return tasks, nil } // ── SSE helpers ─────────────────────────────────────────────────────────────── func sseWrite(w http.ResponseWriter, event, data string) bool { f, ok := w.(http.Flusher) if !ok { return false } if event != "" { fmt.Fprintf(w, "event: %s\n", event) } fmt.Fprintf(w, "data: %s\n\n", data) f.Flush() return true } func sseStart(w http.ResponseWriter) bool { _, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming not supported", http.StatusInternalServerError) return false } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") return true } // streamJob streams lines from a jobState to a SSE response. func streamJob(w http.ResponseWriter, r *http.Request, j *jobState) { if !sseStart(w) { return } streamSubscribedJob(w, r, j) } func streamSubscribedJob(w http.ResponseWriter, r *http.Request, j *jobState) { existing, ch := j.subscribe() for _, line := range existing { sseWrite(w, "", line) } if ch == nil { // Job already finished sseWrite(w, "done", j.err) return } for { select { case line, ok := <-ch: if !ok { sseWrite(w, "done", j.err) return } sseWrite(w, "", line) case <-r.Context().Done(): return } } } // streamCmdJob runs an exec.Cmd and streams stdout+stderr lines into j. func streamCmdJob(j *jobState, cmd *exec.Cmd) error { pr, pw := io.Pipe() cmd.Stdout = pw cmd.Stderr = pw if err := cmd.Start(); err != nil { _ = pw.Close() _ = pr.Close() return err } // Lower the CPU scheduling priority of stress/audit subprocesses to nice+10 // so the X server and kernel interrupt handling remain responsive under load // (prevents KVM/IPMI graphical console from freezing during GPU stress tests). if cmd.Process != nil { _ = syscall.Setpriority(syscall.PRIO_PROCESS, cmd.Process.Pid, 10) } scanDone := make(chan error, 1) go func() { defer func() { if rec := recover(); rec != nil { scanDone <- fmt.Errorf("stream scanner panic: %v", rec) } }() scanner := bufio.NewScanner(pr) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) for scanner.Scan() { // Split on \r to handle progress-bar style output (e.g. \r overwrites) // and strip ANSI escape codes so logs are readable in the browser. parts := strings.Split(scanner.Text(), "\r") for _, part := range parts { line := ansiEscapeRE.ReplaceAllString(part, "") if line != "" { j.append(line) } } } if err := scanner.Err(); err != nil && !errors.Is(err, io.ErrClosedPipe) { scanDone <- err return } scanDone <- nil }() err := cmd.Wait() _ = pw.Close() scanErr := <-scanDone _ = pr.Close() if err != nil { return err } return scanErr } // ── Audit ───────────────────────────────────────────────────────────────────── func (h *handler) handleAPIAuditRun(w http.ResponseWriter, _ *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } t := &Task{ ID: newJobID("audit"), Name: "Audit", Target: "audit", Status: TaskPending, CreatedAt: time.Now(), } globalQueue.enqueue(t) writeJSON(w, map[string]string{"task_id": t.ID, "job_id": t.ID}) } func (h *handler) handleAPIAuditStream(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("job_id") if id == "" { id = r.URL.Query().Get("task_id") } // Try task queue first, then legacy job manager if j, ok := globalQueue.findJob(id); ok { streamJob(w, r, j) return } if j, ok := globalJobs.get(id); ok { streamJob(w, r, j) return } http.Error(w, "job not found", http.StatusNotFound) } // ── SAT ─────────────────────────────────────────────────────────────────────── func (h *handler) handleAPISATRun(target string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var body struct { Duration int `json:"duration"` DiagLevel int `json:"diag_level"` GPUIndices []int `json:"gpu_indices"` ExcludeGPUIndices []int `json:"exclude_gpu_indices"` Loader string `json:"loader"` Profile string `json:"profile"` DisplayName string `json:"display_name"` PlatformComponents []string `json:"platform_components"` } if r.Body != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) { writeError(w, http.StatusBadRequest, "invalid request body") return } } name := taskDisplayName(target, body.Profile, body.Loader) if strings.TrimSpace(body.DisplayName) != "" { name = body.DisplayName } params := taskParams{ Duration: body.Duration, DiagLevel: body.DiagLevel, GPUIndices: body.GPUIndices, ExcludeGPUIndices: body.ExcludeGPUIndices, Loader: body.Loader, BurnProfile: body.Profile, DisplayName: body.DisplayName, PlatformComponents: body.PlatformComponents, } tasks, err := buildNvidiaTaskSet(target, 0, time.Now(), params, name, h.opts.App, "sat-"+target) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } for _, t := range tasks { globalQueue.enqueue(t) } writeTaskRunResponse(w, tasks) } } func (h *handler) handleAPIBenchmarkNvidiaRun(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var body struct { Profile string `json:"profile"` SizeMB int `json:"size_mb"` GPUIndices []int `json:"gpu_indices"` ExcludeGPUIndices []int `json:"exclude_gpu_indices"` RunNCCL *bool `json:"run_nccl"` DisplayName string `json:"display_name"` } if r.Body != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) { writeError(w, http.StatusBadRequest, "invalid request body") return } } runNCCL := true if body.RunNCCL != nil { runNCCL = *body.RunNCCL } name := taskDisplayName("nvidia-benchmark", "", "") if strings.TrimSpace(body.DisplayName) != "" { name = body.DisplayName } tasks, err := buildNvidiaTaskSet("nvidia-benchmark", 15, time.Now(), taskParams{ GPUIndices: body.GPUIndices, ExcludeGPUIndices: body.ExcludeGPUIndices, SizeMB: body.SizeMB, BenchmarkProfile: body.Profile, RunNCCL: runNCCL, DisplayName: body.DisplayName, }, name, h.opts.App, "benchmark-nvidia") if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } for _, t := range tasks { globalQueue.enqueue(t) } writeTaskRunResponse(w, tasks) } func (h *handler) handleAPISATStream(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("job_id") if id == "" { id = r.URL.Query().Get("task_id") } if j, ok := globalQueue.findJob(id); ok { streamJob(w, r, j) return } if j, ok := globalJobs.get(id); ok { streamJob(w, r, j) return } http.Error(w, "job not found", http.StatusNotFound) } func (h *handler) handleAPISATAbort(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("job_id") if id == "" { id = r.URL.Query().Get("task_id") } if t, ok := globalQueue.findByID(id); ok { globalQueue.mu.Lock() switch t.Status { case TaskPending: t.Status = TaskCancelled now := time.Now() t.DoneAt = &now case TaskRunning: if t.job != nil { t.job.abort() } t.Status = TaskCancelled now := time.Now() t.DoneAt = &now } globalQueue.mu.Unlock() writeJSON(w, map[string]string{"status": "aborted"}) return } if j, ok := globalJobs.get(id); ok { if j.abort() { writeJSON(w, map[string]string{"status": "aborted"}) } else { writeJSON(w, map[string]string{"status": "not_running"}) } return } http.Error(w, "job not found", http.StatusNotFound) } // ── Services ────────────────────────────────────────────────────────────────── func (h *handler) handleAPIServicesList(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } names, err := h.opts.App.ListBeeServices() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } type serviceInfo struct { Name string `json:"name"` State string `json:"state"` Body string `json:"body"` } result := make([]serviceInfo, 0, len(names)) for _, name := range names { state := h.opts.App.ServiceState(name) body, _ := h.opts.App.ServiceStatus(name) result = append(result, serviceInfo{Name: name, State: state, Body: body}) } writeJSON(w, result) } func (h *handler) handleAPIServicesAction(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Name string `json:"name"` Action string `json:"action"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } var action platform.ServiceAction switch req.Action { case "start": action = platform.ServiceStart case "stop": action = platform.ServiceStop case "restart": action = platform.ServiceRestart default: writeError(w, http.StatusBadRequest, "action must be start|stop|restart") return } result, err := h.opts.App.ServiceActionResult(req.Name, action) status := "ok" if err != nil { status = "error" } // Always return 200 with output so the frontend can display the actual // systemctl error message instead of a generic "exit status 1". writeJSON(w, map[string]string{"status": status, "output": result.Body}) } // ── Network ─────────────────────────────────────────────────────────────────── func (h *handler) handleAPINetworkStatus(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } ifaces, err := h.opts.App.ListInterfaces() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, map[string]any{ "interfaces": ifaces, "default_route": h.opts.App.DefaultRoute(), "pending_change": h.hasPendingNetworkChange(), "rollback_in": h.pendingNetworkRollbackIn(), }) } func (h *handler) handleAPINetworkDHCP(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Interface string `json:"interface"` } _ = json.NewDecoder(r.Body).Decode(&req) result, err := h.applyPendingNetworkChange(func() (app.ActionResult, error) { if req.Interface == "" || req.Interface == "all" { return h.opts.App.DHCPAllResult() } return h.opts.App.DHCPOneResult(req.Interface) }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, map[string]any{ "status": "ok", "output": result.Body, "rollback_in": int(netRollbackTimeout.Seconds()), }) } func (h *handler) handleAPINetworkStatic(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var req struct { Interface string `json:"interface"` Address string `json:"address"` Prefix string `json:"prefix"` Gateway string `json:"gateway"` DNS []string `json:"dns"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } cfg := platform.StaticIPv4Config{ Interface: req.Interface, Address: req.Address, Prefix: req.Prefix, Gateway: req.Gateway, DNS: req.DNS, } result, err := h.applyPendingNetworkChange(func() (app.ActionResult, error) { return h.opts.App.SetStaticIPv4Result(cfg) }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, map[string]any{ "status": "ok", "output": result.Body, "rollback_in": int(netRollbackTimeout.Seconds()), }) } // ── Export ──────────────────────────────────────────────────────────────────── func (h *handler) handleAPIExportList(w http.ResponseWriter, r *http.Request) { entries, err := listExportFiles(h.opts.ExportDir) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, entries) } func (h *handler) handleAPIExportUSBTargets(w http.ResponseWriter, _ *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } targets, err := h.opts.App.ListRemovableTargets() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if targets == nil { targets = []platform.RemovableTarget{} } writeJSON(w, targets) } func (h *handler) handleAPIExportUSBAudit(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var target platform.RemovableTarget if err := json.NewDecoder(r.Body).Decode(&target); err != nil || target.Device == "" { writeError(w, http.StatusBadRequest, "device is required") return } result, err := h.opts.App.ExportLatestAuditResult(target) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, map[string]string{"status": "ok", "message": result.Body}) } func (h *handler) handleAPIExportUSBBundle(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } var target platform.RemovableTarget if err := json.NewDecoder(r.Body).Decode(&target); err != nil || target.Device == "" { writeError(w, http.StatusBadRequest, "device is required") return } result, err := h.opts.App.ExportSupportBundleResult(target) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, map[string]string{"status": "ok", "message": result.Body}) } // ── GPU presence ────────────────────────────────────────────────────────────── func (h *handler) handleAPIGNVIDIAGPUs(w http.ResponseWriter, _ *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } gpus, err := h.opts.App.ListNvidiaGPUs() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if gpus == nil { gpus = []platform.NvidiaGPU{} } writeJSON(w, gpus) } 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, }) } // ── 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" } _, nvidiaErr := os.Stat("/dev/nvidia0") _, amdErr := os.Stat("/dev/kfd") nvidiaUp := nvidiaErr == nil amdUp := 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.opts.App.LiveBootSource() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(status) } func (h *handler) handleAPIInstallToRAM(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") return } if globalQueue.hasActiveTarget("install") { writeError(w, http.StatusConflict, "install to disk is already running") return } t := &Task{ ID: newJobID("install-to-ram"), Name: "Install to RAM", Target: "install-to-ram", Priority: 10, 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}) } // ── Tools ───────────────────────────────────────────────────────────────────── var standardTools = []string{ "dmidecode", "smartctl", "nvme", "lspci", "ipmitool", "nvidia-smi", "dcgmi", "nv-hostengine", "memtester", "stress-ng", "nvtop", "mstflint", "qrencode", } 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: 20, 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 ─────────────────────────────────────────────────────────────── 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) 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 } // ── Display / Screen Resolution ─────────────────────────────────────────────── type displayMode struct { Output string `json:"output"` Mode string `json:"mode"` Current bool `json:"current"` } type displayInfo struct { Output string `json:"output"` Modes []displayMode `json:"modes"` Current string `json:"current"` } var xrandrOutputRE = regexp.MustCompile(`^(\S+)\s+connected`) var xrandrModeRE = regexp.MustCompile(`^\s{3}(\d+x\d+)\s`) var xrandrCurrentRE = regexp.MustCompile(`\*`) func parseXrandrOutput(out string) []displayInfo { var infos []displayInfo var cur *displayInfo for _, line := range strings.Split(out, "\n") { if m := xrandrOutputRE.FindStringSubmatch(line); m != nil { if cur != nil { infos = append(infos, *cur) } cur = &displayInfo{Output: m[1]} continue } if cur == nil { continue } if m := xrandrModeRE.FindStringSubmatch(line); m != nil { isCurrent := xrandrCurrentRE.MatchString(line) mode := displayMode{Output: cur.Output, Mode: m[1], Current: isCurrent} cur.Modes = append(cur.Modes, mode) if isCurrent { cur.Current = m[1] } } } if cur != nil { infos = append(infos, *cur) } return infos } func xrandrCommand(args ...string) *exec.Cmd { cmd := exec.Command("xrandr", args...) env := append([]string{}, os.Environ()...) hasDisplay := false hasXAuthority := false for _, kv := range env { if strings.HasPrefix(kv, "DISPLAY=") && strings.TrimPrefix(kv, "DISPLAY=") != "" { hasDisplay = true } if strings.HasPrefix(kv, "XAUTHORITY=") && strings.TrimPrefix(kv, "XAUTHORITY=") != "" { hasXAuthority = true } } if !hasDisplay { env = append(env, "DISPLAY=:0") } if !hasXAuthority { env = append(env, "XAUTHORITY=/home/bee/.Xauthority") } cmd.Env = env return cmd } func (h *handler) handleAPIDisplayResolutions(w http.ResponseWriter, _ *http.Request) { out, err := xrandrCommand().Output() if err != nil { writeError(w, http.StatusInternalServerError, "xrandr: "+err.Error()) return } writeJSON(w, parseXrandrOutput(string(out))) } func (h *handler) handleAPIDisplaySet(w http.ResponseWriter, r *http.Request) { var req struct { Output string `json:"output"` Mode string `json:"mode"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Output == "" || req.Mode == "" { writeError(w, http.StatusBadRequest, "output and mode are required") return } // Validate mode looks like WxH to prevent injection if !regexp.MustCompile(`^\d+x\d+$`).MatchString(req.Mode) { writeError(w, http.StatusBadRequest, "invalid mode format") return } // Validate output name (no special chars) if !regexp.MustCompile(`^[A-Za-z0-9_\-]+$`).MatchString(req.Output) { writeError(w, http.StatusBadRequest, "invalid output name") return } if out, err := xrandrCommand("--output", req.Output, "--mode", req.Mode).CombinedOutput(); err != nil { writeError(w, http.StatusInternalServerError, "xrandr: "+strings.TrimSpace(string(out))) return } writeJSON(w, map[string]string{"status": "ok", "output": req.Output, "mode": req.Mode}) }