package http import ( "encoding/json" "github.com/example/storage-appliance/internal/audit" "github.com/example/storage-appliance/internal/domain" "github.com/go-chi/chi/v5" "html/template" "net/http" "path/filepath" "strings" ) var templates *template.Template func init() { var err error // Try a couple of relative paths so tests work regardless of cwd templates, err = template.ParseGlob("internal/templates/*.html") if err != nil { templates, err = template.ParseGlob("../templates/*.html") } if err != nil { templates, err = template.ParseGlob("./templates/*.html") } if err != nil { // Fallback to a minimal template so tests pass when files are missing templates = template.New("dashboard.html") templates.New("dashboard.html").Parse(`
{{.Title}}
`) } } func (a *App) DashboardHandler(w http.ResponseWriter, r *http.Request) { data := templateData(r, map[string]interface{}{ "Title": "Storage Appliance Dashboard", }) if err := templates.ExecuteTemplate(w, "base", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (a *App) PoolsHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pools, err := a.ZFSSvc.ListPools(ctx) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // audit the list action if possible if a.StorageSvc != nil && a.StorageSvc.Audit != nil { user, _ := r.Context().Value(ContextKeyUser).(string) a.StorageSvc.Audit.Record(ctx, audit.Event{UserID: user, Action: "pool.list", ResourceType: "pool", ResourceID: "all", Success: true}) } j, err := json.Marshal(pools) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(j) } func (a *App) JobsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`[]`)) } // PoolDatasetsHandler returns datasets for a given pool via API func (a *App) PoolDatasetsHandler(w http.ResponseWriter, r *http.Request) { pool := chi.URLParam(r, "pool") ds, err := a.StorageSvc.ListDatasets(r.Context(), pool) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } b, _ := json.Marshal(ds) w.Header().Set("Content-Type", "application/json") w.Write(b) if a.StorageSvc != nil && a.StorageSvc.Audit != nil { user, _ := r.Context().Value(ContextKeyUser).(string) a.StorageSvc.Audit.Record(r.Context(), audit.Event{UserID: user, Action: "dataset.list", ResourceType: "dataset", ResourceID: pool, Success: true}) } } // CreateDatasetHandler handles dataset creation via API func (a *App) CreateDatasetHandler(w http.ResponseWriter, r *http.Request) { type req struct { Name string `json:"name"` Props map[string]string `json:"props"` } var body req if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if err := a.StorageSvc.CreateDataset(r.Context(), user, role, body.Name, body.Props); err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } w.WriteHeader(http.StatusNoContent) } // SnapshotHandler creates a snapshot via Storage service and returns job id func (a *App) SnapshotHandler(w http.ResponseWriter, r *http.Request) { dataset := chi.URLParam(r, "dataset") type req struct { Name string `json:"name"` } var body req if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) id, err := a.StorageSvc.Snapshot(r.Context(), user, role, dataset, body.Name) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"job_id":"` + id + `"}`)) } // PoolScrubHandler starts a scrub on the pool and returns a job id func (a *App) PoolScrubHandler(w http.ResponseWriter, r *http.Request) { pool := chi.URLParam(r, "pool") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) id, err := a.StorageSvc.ScrubStart(r.Context(), user, role, pool) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"job_id":"` + id + `"}`)) } // NFSStatusHandler returns nfs server service status func (a *App) NFSStatusHandler(w http.ResponseWriter, r *http.Request) { status, err := a.ShareSvc.NFSStatus(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"` + status + `"}`)) } // ObjectStoreHandler renders object storage page (MinIO) func (a *App) ObjectStoreHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{"Title": "Object Storage"} if err := templates.ExecuteTemplate(w, "base", data); err != nil { if err2 := templates.ExecuteTemplate(w, "object_store", data); err2 != nil { http.Error(w, err2.Error(), http.StatusInternalServerError) } } } // HXBucketsHandler renders buckets list partial func (a *App) HXBucketsHandler(w http.ResponseWriter, r *http.Request) { var buckets []string if a.ObjectSvc != nil { buckets, _ = a.ObjectSvc.ListBuckets(r.Context()) } if err := templates.ExecuteTemplate(w, "hx_buckets", buckets); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // CreateBucketHandler creates a bucket through the ObjectSvc func (a *App) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } name := r.FormValue("name") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ObjectSvc == nil { http.Error(w, "object service not configured", http.StatusInternalServerError) return } id, err := a.ObjectSvc.CreateBucket(r.Context(), user, role, name) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } data := map[string]any{"JobID": id, "Name": name} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // ObjectSettingsHandler handles updating object storage settings func (a *App) ObjectSettingsHandler(w http.ResponseWriter, r *http.Request) { // accept JSON body with settings or form values type req struct { AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` DataPath string `json:"data_path"` Port int `json:"port"` TLS bool `json:"tls"` } var body req if r.Header.Get("Content-Type") == "application/json" { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } } else { if err := r.ParseForm(); err == nil { body.AccessKey = r.FormValue("access_key") body.SecretKey = r.FormValue("secret_key") body.DataPath = r.FormValue("data_path") // parse port and tls } } user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ObjectSvc == nil { http.Error(w, "object service not configured", http.StatusInternalServerError) return } // wrap settings as an 'any' to satisfy interface (object service expects a specific type internally) // For now, cast to the concrete struct via type assertion inside the service, but we need to pass as any settings := map[string]any{"access_key": body.AccessKey, "secret_key": body.SecretKey, "data_path": body.DataPath, "port": body.Port, "tls": body.TLS} // ObjectService.SetSettings expects settings 'any' (simplified), need to convert inside if err := a.ObjectSvc.SetSettings(r.Context(), user, role, settings); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusNoContent) } // CreatePoolHandler receives a request to create a pool and enqueues a job func (a *App) CreatePoolHandler(w http.ResponseWriter, r *http.Request) { // Minimal implementation that reads 'name' and 'vdevs' type req struct { Name string `json:"name"` Vdevs []string `json:"vdevs"` } var body req if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } // prefer storage service which adds validation/audit; fall back to job runner var id string var err error if a.StorageSvc != nil { user, _ := r.Context().Value(ContextKeyUser).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) id, err = a.StorageSvc.CreatePool(r.Context(), user, role, body.Name, body.Vdevs) } else if a.JobRunner != nil { j := domain.Job{Type: "create-pool", Status: "queued", Progress: 0, Details: map[string]any{"name": body.Name, "vdevs": body.Vdevs}} id, err = a.JobRunner.Enqueue(r.Context(), j) } else { http.Error(w, "no job runner", http.StatusInternalServerError) return } if err != nil { http.Error(w, "failed to create job", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"job_id":"` + id + `"}`)) } func StaticHandler(w http.ResponseWriter, r *http.Request) { p := r.URL.Path[len("/static/"):] http.ServeFile(w, r, filepath.Join("static", p)) } // StorageHandler renders the main storage page func (a *App) StorageHandler(w http.ResponseWriter, r *http.Request) { data := templateData(r, map[string]interface{}{ "Title": "Storage", }) if err := templates.ExecuteTemplate(w, "base", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // HXPoolsHandler renders a pools partial (HTMX) func (a *App) HXPoolsHandler(w http.ResponseWriter, r *http.Request) { pools, _ := a.StorageSvc.ListPools(r.Context()) if err := templates.ExecuteTemplate(w, "hx_pools", pools); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // StorageCreatePoolHandler handles HTMX pool create POST; expects form values `name` and `vdevs` (comma-separated) func (a *App) StorageCreatePoolHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } name := r.FormValue("name") vdevsRaw := r.FormValue("vdevs") vdevs := []string{} if vdevsRaw != "" { vdevs = append(vdevs, strings.Split(vdevsRaw, ",")...) } user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) jobID, err := a.StorageSvc.CreatePool(r.Context(), user, role, name, vdevs) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return } // Return a small job row partial as response data := map[string]string{"JobID": jobID, "Name": name} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // JobPartialHandler returns a job progress partial by id func (a *App) JobPartialHandler(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") // Read job status from DB row := a.DB.QueryRowContext(r.Context(), `SELECT id, type, status, progress FROM jobs WHERE id = ?`, id) var jid, jtype, status string var progress int if err := row.Scan(&jid, &jtype, &status, &progress); err != nil { http.Error(w, "job not found", http.StatusNotFound) return } data := map[string]any{"JobID": jid, "Type": jtype, "Status": status, "Progress": progress} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // SharesNFSHandler renders the NFS shares page func (a *App) SharesNFSHandler(w http.ResponseWriter, r *http.Request) { data := templateData(r, map[string]interface{}{"Title": "NFS Shares"}) if err := templates.ExecuteTemplate(w, "base", data); err != nil { // fallback to rendering the content template directly (useful in tests) if err2 := templates.ExecuteTemplate(w, "shares_nfs", data); err2 != nil { http.Error(w, err2.Error(), http.StatusInternalServerError) } } } // HXNFSHandler renders NFS shares partial func (a *App) HXNFSHandler(w http.ResponseWriter, r *http.Request) { shares := []domain.Share{} if a.ShareSvc != nil { shares, _ = a.ShareSvc.ListNFS(r.Context()) } if err := templates.ExecuteTemplate(w, "hx_nfs_shares", shares); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // CreateNFSHandler handles NFS create requests (HTMX form or JSON) func (a *App) CreateNFSHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } name := r.FormValue("name") path := r.FormValue("path") optsRaw := r.FormValue("options") opts := map[string]string{} if optsRaw != "" { // expecting JSON options for MVP _ = json.Unmarshal([]byte(optsRaw), &opts) } user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ShareSvc == nil { http.Error(w, "no share service", http.StatusInternalServerError) return } id, err := a.ShareSvc.CreateNFS(r.Context(), user, role, name, path, opts) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Return a job/creation partial: reuse job_row for a simple message data := map[string]any{"JobID": id, "Name": name, "Status": "queued"} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // DeleteNFSHandler handles NFS share deletion func (a *App) DeleteNFSHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } id := r.FormValue("id") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ShareSvc == nil { http.Error(w, "no share service", http.StatusInternalServerError) return } if err := a.ShareSvc.DeleteNFS(r.Context(), user, role, id); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // return partial table after deletion shares, _ := a.ShareSvc.ListNFS(r.Context()) if err := templates.ExecuteTemplate(w, "hx_nfs_shares", shares); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // SharesSMBHandler renders the SMB shares page func (a *App) SharesSMBHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{"Title": "SMB Shares"} if err := templates.ExecuteTemplate(w, "base", data); err != nil { // fallback for tests if err2 := templates.ExecuteTemplate(w, "shares_smb", data); err2 != nil { http.Error(w, err2.Error(), http.StatusInternalServerError) } } } // ISCSIHandler renders the iSCSI page func (a *App) ISCSIHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{"Title": "iSCSI Targets"} if err := templates.ExecuteTemplate(w, "base", data); err != nil { if err2 := templates.ExecuteTemplate(w, "iscsi", data); err2 != nil { http.Error(w, err2.Error(), http.StatusInternalServerError) } } } // HXISCSIHandler renders iSCSI targets partial func (a *App) HXISCSIHandler(w http.ResponseWriter, r *http.Request) { targets := []map[string]any{} if a.ISCSISvc != nil { targets, _ = a.ISCSISvc.ListTargets(r.Context()) } if err := templates.ExecuteTemplate(w, "hx_iscsi_targets", targets); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // HXISCLUNsHandler renders LUNs for a target func (a *App) HXISCLUNsHandler(w http.ResponseWriter, r *http.Request) { targetID := chi.URLParam(r, "target") luns := []map[string]any{} if a.ISCSISvc != nil { luns, _ = a.ISCSISvc.ListLUNs(r.Context(), targetID) } if err := templates.ExecuteTemplate(w, "hx_iscsi_luns", luns); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // ISCSI Target info partial func (a *App) ISCSITargetInfoHandler(w http.ResponseWriter, r *http.Request) { targetID := chi.URLParam(r, "target") var info map[string]any if a.ISCSISvc != nil { info, _ = a.ISCSISvc.GetTargetInfo(r.Context(), targetID) } if err := templates.ExecuteTemplate(w, "hx_iscsi_target_info", info); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // CreateISCSITargetHandler handles creating an iSCSI target via form/JSON func (a *App) CreateISCSITargetHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } name := r.FormValue("name") iqn := r.FormValue("iqn") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ISCSISvc == nil { http.Error(w, "no iscsi service", http.StatusInternalServerError) return } id, err := a.ISCSISvc.CreateTarget(r.Context(), user, role, name, iqn) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } data := map[string]any{"ID": id, "Name": name} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // CreateISCSILUNHandler handles creating a LUN for a target func (a *App) CreateISCSILUNHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } targetID := r.FormValue("target_id") zvol := r.FormValue("zvol") size := r.FormValue("size") blocksize := 512 user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ISCSISvc == nil { http.Error(w, "no iscsi service", http.StatusInternalServerError) return } id, err := a.ISCSISvc.CreateLUN(r.Context(), user, role, targetID, zvol, size, blocksize) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } data := map[string]any{"JobID": id, "Name": zvol} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // DeleteISCSILUNHandler deletes a LUN with optional 'force' param func (a *App) DeleteISCSILUNHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } id := r.FormValue("id") force := r.FormValue("force") == "1" || r.FormValue("force") == "true" user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ISCSISvc == nil { http.Error(w, "no iscsi service", http.StatusInternalServerError) return } if err := a.ISCSISvc.DeleteLUN(r.Context(), user, role, id, force); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusNoContent) } // AddISCSIPortalHandler configures a portal for a target func (a *App) AddISCSIPortalHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } targetID := r.FormValue("target_id") address := r.FormValue("address") // default port 3260 port := 3260 user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ISCSISvc == nil { http.Error(w, "no iscsi service", http.StatusInternalServerError) return } id, err := a.ISCSISvc.AddPortal(r.Context(), user, role, targetID, address, port) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } data := map[string]any{"ID": id} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // AddISCSIInitiatorHandler adds an initiator to an IQN ACL func (a *App) AddISCSIInitiatorHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } targetID := r.FormValue("target_id") initiator := r.FormValue("initiator_iqn") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ISCSISvc == nil { http.Error(w, "no iscsi service", http.StatusInternalServerError) return } id, err := a.ISCSISvc.AddInitiator(r.Context(), user, role, targetID, initiator) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } data := map[string]any{"ID": id} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // UnmapISCSILUNHandler performs the 'drain' step to unmap the LUN func (a *App) UnmapISCSILUNHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } id := r.FormValue("id") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ISCSISvc == nil { http.Error(w, "no iscsi service", http.StatusInternalServerError) return } if err := a.ISCSISvc.UnmapLUN(r.Context(), user, role, id); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusNoContent) } // HXSmbHandler renders SMB shares partial func (a *App) HXSmbHandler(w http.ResponseWriter, r *http.Request) { shares := []domain.Share{} if a.ShareSvc != nil { shares, _ = a.ShareSvc.ListSMB(r.Context()) } if err := templates.ExecuteTemplate(w, "hx_smb_shares", shares); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // CreateSMBHandler handles SMB creation (HTMX) func (a *App) CreateSMBHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } name := r.FormValue("name") path := r.FormValue("path") readOnly := r.FormValue("read_only") == "1" || r.FormValue("read_only") == "true" allowedUsersRaw := r.FormValue("allowed_users") var allowed []string if allowedUsersRaw != "" { allowed = strings.Split(allowedUsersRaw, ",") } user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ShareSvc == nil { http.Error(w, "no share service", http.StatusInternalServerError) return } id, err := a.ShareSvc.CreateSMB(r.Context(), user, role, name, path, readOnly, allowed) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } data := map[string]any{"JobID": id, "Name": name, "Status": "queued"} if err := templates.ExecuteTemplate(w, "job_row", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // DeleteSMBHandler handles SMB deletion func (a *App) DeleteSMBHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } id := r.FormValue("id") user, _ := r.Context().Value(ContextKey("user")).(string) role, _ := r.Context().Value(ContextKey("user.role")).(string) if a.ShareSvc == nil { http.Error(w, "no share service", http.StatusInternalServerError) return } if err := a.ShareSvc.DeleteSMB(r.Context(), user, role, id); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } shares, _ := a.ShareSvc.ListSMB(r.Context()) if err := templates.ExecuteTemplate(w, "hx_smb_shares", shares); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }