controller v0.45.0: storage UX polish — deterministic order, init filter, register shortcut, system-storage clarity

B1 sort /api/disks (user-data→system→backup, alpha within); B2 init wizard
excludes mounted drives; B3 Regisztrálás primary action for mounted-unregistered
user-data drives (POST /api/storage/register); B4 per-card purpose descriptions +
app-backing tags + tiering note (local & local-lvm both kept); B5 eject already
names affected apps. Pairs with felhom-agent v0.24.0 eject role-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 09:35:31 +02:00
parent 12064dcd88
commit 9ed844fd0b
8 changed files with 186 additions and 10 deletions
@@ -185,6 +185,8 @@ func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
s.handleStorageWipe(w, r)
case r.URL.Path == "/api/storage/impact" && r.Method == http.MethodGet:
s.handleStorageImpact(w, r)
case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost:
s.handleStorageRegister(w, r)
default:
http.NotFound(w, r)
}
@@ -325,6 +327,35 @@ func (s *Server) handleStorageWipe(w http.ResponseWriter, r *http.Request) {
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"device": req.Device, "wiped": fr.Formatted, "durable_id": fr.DurableID})
}
// handleStorageRegister records an ALREADY-mounted, unregistered user-data drive into the StoragePath
// registry — no format, no eject. It is the natural primary action for a mounted-but-unregistered data
// drive (e.g. felhom-usb): the customer's intent is to USE the existing data, not wipe it. It reuses
// registerStoragePath (the manual-add path) — AddStoragePath dedupes, so a double-register is a clean
// error, not a duplicate.
func (s *Server) handleStorageRegister(w http.ResponseWriter, r *http.Request) {
var req struct {
Where string `json:"where"`
Label string `json:"label"`
SetDefault bool `json:"set_default"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen kérés", nil)
return
}
req.Where = path.Clean(strings.TrimSpace(req.Where))
if req.Where == "" || req.Where == "." || !strings.HasPrefix(req.Where, "/mnt/") {
writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen csatlakoztatási pont", nil)
return
}
if err := s.registerStoragePath(req.Where, req.Label, req.SetDefault); err != nil {
s.logger.Printf("[WARN] [web] storage register %s failed: %v", req.Where, err)
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
return
}
s.logger.Printf("[INFO] [web] storage path registered (existing mount): %s", req.Where)
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"registered": true, "where": req.Where})
}
func (s *Server) handleStorageAttach(w http.ResponseWriter, r *http.Request) {
var req storageProvReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {