package webui import ( "errors" "fmt" "html" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "bee/audit/internal/app" "reanimator/chart/viewer" chartweb "reanimator/chart/web" ) const defaultTitle = "Bee Hardware Audit" type HandlerOptions struct { Title string AuditPath string ExportDir string } func NewHandler(opts HandlerOptions) http.Handler { title := strings.TrimSpace(opts.Title) if title == "" { title = defaultTitle } auditPath := strings.TrimSpace(opts.AuditPath) exportDir := strings.TrimSpace(opts.ExportDir) if exportDir == "" { exportDir = app.DefaultExportDir } mux := http.NewServeMux() mux.Handle("GET /static/", http.StripPrefix("/static/", chartweb.Static())) mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) mux.HandleFunc("GET /audit.json", func(w http.ResponseWriter, r *http.Request) { data, err := loadSnapshot(auditPath) if err != nil { if errors.Is(err, os.ErrNotExist) { http.Error(w, "audit snapshot not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("read audit snapshot: %v", err), http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json; charset=utf-8") _, _ = w.Write(data) }) mux.HandleFunc("GET /export/support.tar.gz", func(w http.ResponseWriter, r *http.Request) { archive, err := app.BuildSupportBundle(exportDir) if err != nil { http.Error(w, fmt.Sprintf("build support bundle: %v", err), http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/gzip") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(archive))) http.ServeFile(w, r, archive) }) mux.HandleFunc("GET /runtime-health.json", func(w http.ResponseWriter, r *http.Request) { data, err := loadSnapshot(filepath.Join(exportDir, "runtime-health.json")) if err != nil { if errors.Is(err, os.ErrNotExist) { http.Error(w, "runtime health not found", http.StatusNotFound) return } http.Error(w, fmt.Sprintf("read runtime health: %v", err), http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json; charset=utf-8") _, _ = w.Write(data) }) mux.HandleFunc("GET /export/", func(w http.ResponseWriter, r *http.Request) { body, err := renderExportIndex(exportDir) if err != nil { http.Error(w, fmt.Sprintf("render export index: %v", err), http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(body)) }) mux.HandleFunc("GET /export/file", func(w http.ResponseWriter, r *http.Request) { rel := strings.TrimSpace(r.URL.Query().Get("path")) if rel == "" { http.Error(w, "path is required", http.StatusBadRequest) return } clean := filepath.Clean(rel) if clean == "." || strings.HasPrefix(clean, "..") { http.Error(w, "invalid path", http.StatusBadRequest) return } http.ServeFile(w, r, filepath.Join(exportDir, clean)) }) mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { snapshot, err := loadSnapshot(auditPath) if err != nil && !errors.Is(err, os.ErrNotExist) { http.Error(w, fmt.Sprintf("read audit snapshot: %v", err), http.StatusInternalServerError) return } noticeTitle, noticeBody := runtimeNotice(filepath.Join(exportDir, "runtime-health.json")) html, err := viewer.RenderHTMLWithOptions(snapshot, title, viewer.RenderOptions{ DownloadArchiveURL: "/export/support.tar.gz", DownloadArchiveLabel: "Download support bundle", NoticeTitle: noticeTitle, NoticeBody: noticeBody, }) if err != nil { http.Error(w, fmt.Sprintf("render snapshot: %v", err), http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(html) }) return mux } func ListenAndServe(addr string, opts HandlerOptions) error { return http.ListenAndServe(addr, NewHandler(opts)) } func loadSnapshot(path string) ([]byte, error) { if strings.TrimSpace(path) == "" { return nil, os.ErrNotExist } return os.ReadFile(path) } func runtimeNotice(path string) (string, string) { health, err := app.ReadRuntimeHealth(path) if err != nil { return "Runtime Health", "No runtime health snapshot found yet." } body := fmt.Sprintf("Status: %s. Export dir: %s. Driver ready: %t. CUDA ready: %t. Network: %s. Export files: /export/", firstNonEmpty(health.Status, "UNKNOWN"), firstNonEmpty(health.ExportDir, app.DefaultExportDir), health.DriverReady, health.CUDAReady, firstNonEmpty(health.NetworkStatus, "UNKNOWN"), ) if len(health.Issues) > 0 { body += " Issues: " parts := make([]string, 0, len(health.Issues)) for _, issue := range health.Issues { parts = append(parts, issue.Code) } body += strings.Join(parts, ", ") } return "Runtime Health", body } func renderExportIndex(exportDir string) (string, error) { var entries []string err := filepath.Walk(strings.TrimSpace(exportDir), func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } rel, err := filepath.Rel(exportDir, path) if err != nil { return err } entries = append(entries, rel) return nil }) if err != nil && !errors.Is(err, os.ErrNotExist) { return "", err } sort.Strings(entries) var body strings.Builder body.WriteString("Bee Export Files") body.WriteString("

Bee Export Files

") return body.String(), nil } func firstNonEmpty(value, fallback string) string { value = strings.TrimSpace(value) if value == "" { return fallback } return value }