feat(webui): add Install to Disk page
Expose the existing bee-install script through the web UI: - platform/install.go: remove USB exclusion, add SizeBytes/MountedParts fields, add MinInstallBytes()/DiskWarnings() safety checks (size, mounted partitions, toram+low-RAM warning) - webui: add GET /api/install/disks, POST /api/install/run, GET /api/install/stream endpoints - webui: add Install to Disk page with disk table, warning badges, device-name confirmation gate, SSE progress terminal, reboot button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -409,6 +409,101 @@ func (h *handler) handleAPIPreflight(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = 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
|
||||
}
|
||||
|
||||
h.installMu.Lock()
|
||||
if h.installJob != nil && !h.installJob.isDone() {
|
||||
h.installMu.Unlock()
|
||||
writeError(w, http.StatusConflict, "install already running")
|
||||
return
|
||||
}
|
||||
j := &jobState{}
|
||||
h.installJob = j
|
||||
h.installMu.Unlock()
|
||||
|
||||
logFile := platform.InstallLogPath(req.Device)
|
||||
go runCmdJob(j, exec.CommandContext(r.Context(), "bee-install", req.Device, logFile))
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *handler) handleAPIInstallStream(w http.ResponseWriter, r *http.Request) {
|
||||
h.installMu.Lock()
|
||||
j := h.installJob
|
||||
h.installMu.Unlock()
|
||||
if j == nil {
|
||||
if !sseStart(w) {
|
||||
return
|
||||
}
|
||||
sseWrite(w, "done", "")
|
||||
return
|
||||
}
|
||||
streamJob(w, r, j)
|
||||
}
|
||||
|
||||
// ── Metrics SSE ───────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *handler) handleAPIMetricsStream(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user