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:
@@ -1,5 +1,30 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.45.0 — storage UX polish: deterministic order, init filter, register shortcut, system-storage clarity (2026-06-12)
|
||||||
|
|
||||||
|
Builds on v0.44.0's role-aware drive management. Pairs with felhom-agent v0.24.0 (the eject role-gate
|
||||||
|
lives at the agent — see its CHANGELOG). This release is the controller-side clarity/ordering polish.
|
||||||
|
|
||||||
|
- **Deterministic disk order (B1)** — `GET /api/disks` now sorts the agent's drive list server-side:
|
||||||
|
**user-data → system → backup** (then unrecognized), alphabetical by storage name within each tier.
|
||||||
|
The agent's storage view iterates an unordered Go map, so the list previously reordered on every
|
||||||
|
reload (CLAUDE.md lesson #3). The customer's manageable drives are now always on top, stably.
|
||||||
|
`sortDisksForView` in `agent_disk_handlers.go` + `TestSortDisksForView`.
|
||||||
|
- **Init wizard excludes mounted drives (B2)** — `storage_init.html`'s formattable filter gained
|
||||||
|
`&& !d.mount_path`, matching the attach wizard: an already-mounted drive (e.g. `felhom-usb`) no
|
||||||
|
longer appears as an "initialize" candidate. Eject it first to make it an init target.
|
||||||
|
- **Register shortcut (B3)** — a mounted, unregistered **user-data** drive now offers **Regisztrálás**
|
||||||
|
as its PRIMARY per-card action (Leválasztás/Törlés stay secondary). It records the existing mount
|
||||||
|
into the `StoragePath` registry (no format, no eject) via the new `POST /api/storage/register` →
|
||||||
|
`registerStoragePath`, then FileBrowser syncs. The natural "use this drive" intent, not "wipe it".
|
||||||
|
- **System-storage clarity (B4)** — `local` and `local-lvm` are both kept (not collapsed); each
|
||||||
|
storage card now carries a plain-Hungarian **purpose description** keyed on the agent's role/type,
|
||||||
|
the app-backing storages (`local-lvm` → "Alkalmazás-rendszer"; user-data → "Alkalmazás-adatok") are
|
||||||
|
tagged, and a one-line tiering note above the list answers "which storage do the apps use?". Pure
|
||||||
|
controller-side presentation — no agent contract change; role/type stay authoritative from the agent.
|
||||||
|
- **Eject impact (B5)** — the eject confirmation already lists, by name, the deployed apps that lose
|
||||||
|
their storage (via `/api/storage/impact`), at parity with the wipe warning — verified, no change.
|
||||||
|
|
||||||
### v0.44.0 — role-aware drive management: protected lockout + customer type-to-confirm wipe + drive-list restyle (2026-06-11)
|
### v0.44.0 — role-aware drive management: protected lockout + customer type-to-confirm wipe + drive-list restyle (2026-06-11)
|
||||||
|
|
||||||
The controller half of the storage-authorization redesign. The drive UI is now driven by the agent's
|
The controller half of the storage-authorization redesign. The drive UI is now driven by the agent's
|
||||||
|
|||||||
+17
-2
@@ -514,7 +514,8 @@ not just those with HDD data. Non-HDD apps can configure destination, method, an
|
|||||||
|
|
||||||
### 4. Storage Management
|
### 4. Storage Management
|
||||||
|
|
||||||
> **⚠️ Rebuilt on the agent-delegated disk model (v0.43.0), made ROLE-AWARE in v0.44.0.** After the 8C
|
> **⚠️ Rebuilt on the agent-delegated disk model (v0.43.0), made ROLE-AWARE in v0.44.0, UX-polished in
|
||||||
|
> v0.45.0.** After the 8C
|
||||||
> de-privileging, the controller holds **no Proxmox/disk credentials and no destructive authority** — disk
|
> de-privileging, the controller holds **no Proxmox/disk credentials and no destructive authority** — disk
|
||||||
> execution + the gate live entirely in the **host agent**. The drive UI is driven by the agent's
|
> execution + the gate live entirely in the **host agent**. The drive UI is driven by the agent's
|
||||||
> authoritative **role** (`system` | `backup` | `user-data`, from `GET /api/disks`): the appliance's own
|
> authoritative **role** (`system` | `backup` | `user-data`, from `GET /api/disks`): the appliance's own
|
||||||
@@ -526,6 +527,17 @@ not just those with HDD data. Non-HDD apps can configure destination, method, an
|
|||||||
> Biztonsági mentés — védett / Felhasználói adat) and registered state, plus a **capacity bar** (the
|
> Biztonsági mentés — védett / Felhasználói adat) and registered state, plus a **capacity bar** (the
|
||||||
> monitoring `system-bar`, from the agent's `total_bytes`/`used_bytes`). Eject/Wipe render **only** for
|
> monitoring `system-bar`, from the agent's `total_bytes`/`used_bytes`). Eject/Wipe render **only** for
|
||||||
> user-data drives mounted under `/mnt`.
|
> user-data drives mounted under `/mnt`.
|
||||||
|
> - **(v0.45.0) Deterministic order** — `agentDisksListHandler` sorts the list server-side
|
||||||
|
> (`sortDisksForView`): **user-data → system → backup** (then unrecognized), alpha by name within a
|
||||||
|
> tier, so it no longer reorders on each reload (the agent's view iterates an unordered Go map).
|
||||||
|
> - **(v0.45.0) Purpose + app-backing clarity (B4)** — `local` and `local-lvm` are both shown (not
|
||||||
|
> collapsed); each card carries a plain-Hungarian **purpose description** keyed on the agent's
|
||||||
|
> role/type, the app-backing storages are tagged (`local-lvm` → "Alkalmazás-rendszer"; user-data →
|
||||||
|
> "Alkalmazás-adatok"), and a one-line tiering note above the list answers "which storage do the
|
||||||
|
> apps use?". Pure presentation — role/type stay authoritative from the agent.
|
||||||
|
> - **(v0.45.0) Register shortcut (B3)** — a mounted, **unregistered** user-data drive offers
|
||||||
|
> **Regisztrálás** as its PRIMARY action: `POST /api/storage/register` → `registerStoragePath` records
|
||||||
|
> the existing mount (no format, no eject) + FileBrowser-syncs. Leválasztás/Törlés stay secondary.
|
||||||
> - **Customer wipe/eject** — a **type-to-confirm** modal that names the deployed apps that break
|
> - **Customer wipe/eject** — a **type-to-confirm** modal that names the deployed apps that break
|
||||||
> (`GET /api/storage/impact` → `appsUsingPath`) and disables the destructive button until the **mount
|
> (`GET /api/storage/impact` → `appsUsingPath`) and disables the destructive button until the **mount
|
||||||
> name is typed exactly**. Wipe (`POST /api/storage/wipe`): eject (unmount + deregister) → server-side
|
> name is typed exactly**. Wipe (`POST /api/storage/wipe`): eject (unmount + deregister) → server-side
|
||||||
@@ -538,7 +550,10 @@ not just those with HDD data. Non-HDD apps can configure destination, method, an
|
|||||||
> only if a protected device somehow reaches init.
|
> only if a protected device somehow reaches init.
|
||||||
> - **Guided attach** (`/settings/storage/attach`, `POST /api/storage/attach`): non-destructive — resolve
|
> - **Guided attach** (`/settings/storage/attach`, `POST /api/storage/attach`): non-destructive — resolve
|
||||||
> the existing fs UUID → `assign` → register. Selector restyled to cards (user-data only).
|
> the existing fs UUID → `assign` → register. Selector restyled to cards (user-data only).
|
||||||
> - **Eject** (`POST /api/storage/eject`): benign unmount + deregister, with the agent's dependent-guest warning.
|
> - **Eject** (`POST /api/storage/eject`): benign unmount + deregister, with the agent's dependent-guest
|
||||||
|
> warning + the affected-app list (parity with wipe). **The eject is ROLE-GATED at the agent** (felhom-
|
||||||
|
> agent v0.24.0): `POST /disks/eject` refuses to unmount a system/backup mount — the UI hiding the button
|
||||||
|
> is defense-in-depth, not the control. Only user-data mounts are ejectable.
|
||||||
> - **`agentapi`** (`internal/agentapi`) is the pinned client to the agent local API: `Disks`/`AssignDisk`/
|
> - **`agentapi`** (`internal/agentapi`) is the pinned client to the agent local API: `Disks`/`AssignDisk`/
|
||||||
> `EjectDisk`/`FormatDisk(…, confirmed, durableID)`; `DiskInfo.role`+capacity;
|
> `EjectDisk`/`FormatDisk(…, confirmed, durableID)`; `DiskInfo.role`+capacity;
|
||||||
> `FormatResult.{role,needs_confirmation,durable_id}`; `ErrNeedsConfirmation` (user-data) vs
|
> `FormatResult.{role,needs_confirmation,durable_id}`; `ErrNeedsConfirmation` (user-data) vs
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||||
)
|
)
|
||||||
@@ -73,9 +74,39 @@ func (s *Server) agentDisksListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Deterministic order: the agent's storage view iterates a Go map (unordered), so the list would
|
||||||
|
// otherwise reorder on every reload (CLAUDE.md lesson #3). The customer's manageable drives go on
|
||||||
|
// top, in a stable order: user-data first, then system, then backup, alpha by name within a tier.
|
||||||
|
sortDisksForView(resp.Disks)
|
||||||
writeDiskJSON(w, http.StatusOK, true, "", resp)
|
writeDiskJSON(w, http.StatusOK, true, "", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sortDisksForView orders the agent's disk list deterministically (user-data → system → backup →
|
||||||
|
// unrecognized; alphabetical by storage name within each tier). A stable Go-side contract beats
|
||||||
|
// relying on map iteration order or template JS alone.
|
||||||
|
func sortDisksForView(disks []agentapi.DiskInfo) {
|
||||||
|
sort.SliceStable(disks, func(i, j int) bool {
|
||||||
|
if ri, rj := diskRoleRank(disks[i].Role), diskRoleRank(disks[j].Role); ri != rj {
|
||||||
|
return ri < rj
|
||||||
|
}
|
||||||
|
return disks[i].Name < disks[j].Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// diskRoleRank ranks a role for the overview ordering (lower sorts first).
|
||||||
|
func diskRoleRank(role string) int {
|
||||||
|
switch role {
|
||||||
|
case "user-data":
|
||||||
|
return 0
|
||||||
|
case "system":
|
||||||
|
return 1
|
||||||
|
case "backup":
|
||||||
|
return 2
|
||||||
|
default:
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// agentDiskAssignHandler proxies POST /api/disks/assign → agent POST /disks/assign.
|
// agentDiskAssignHandler proxies POST /api/disks/assign → agent POST /disks/assign.
|
||||||
func (s *Server) agentDiskAssignHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) agentDiskAssignHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleStorageWipe(w, r)
|
s.handleStorageWipe(w, r)
|
||||||
case r.URL.Path == "/api/storage/impact" && r.Method == http.MethodGet:
|
case r.URL.Path == "/api/storage/impact" && r.Method == http.MethodGet:
|
||||||
s.handleStorageImpact(w, r)
|
s.handleStorageImpact(w, r)
|
||||||
|
case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost:
|
||||||
|
s.handleStorageRegister(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
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})
|
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) {
|
func (s *Server) handleStorageAttach(w http.ResponseWriter, r *http.Request) {
|
||||||
var req storageProvReq
|
var req storageProvReq
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
|||||||
@@ -237,6 +237,34 @@ func TestAppsUsingPathIn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// B1: the disk overview must render in a deterministic order — user-data first, then system, then
|
||||||
|
// backup (then anything unrecognized), alphabetical by name within each tier — so the list does not
|
||||||
|
// reorder on each reload (the agent's storage view iterates an unordered Go map).
|
||||||
|
func TestSortDisksForView(t *testing.T) {
|
||||||
|
disks := []agentapi.DiskInfo{
|
||||||
|
{Name: "felhom-pbs", Role: "backup"},
|
||||||
|
{Name: "local-lvm", Role: "system"},
|
||||||
|
{Name: "zdata", Role: "user-data"},
|
||||||
|
{Name: "local", Role: "system"},
|
||||||
|
{Name: "adata", Role: "user-data"},
|
||||||
|
{Name: "mystery", Role: ""},
|
||||||
|
}
|
||||||
|
sortDisksForView(disks)
|
||||||
|
var got []string
|
||||||
|
for _, d := range disks {
|
||||||
|
got = append(got, d.Name)
|
||||||
|
}
|
||||||
|
want := []string{"adata", "zdata", "local", "local-lvm", "felhom-pbs", "mystery"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("length mismatch: got %v want %v", got, want)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("order at %d: got %q want %q (full: %v)", i, got[i], want[i], got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMountWhere(t *testing.T) {
|
func TestMountWhere(t *testing.T) {
|
||||||
if w, err := mountWhere("hdd_1"); err != nil || w != "/mnt/hdd_1" {
|
if w, err := mountWhere("hdd_1"); err != nil || w != "/mnt/hdd_1" {
|
||||||
t.Errorf("mountWhere(hdd_1) = %q, %v", w, err)
|
t.Errorf("mountWhere(hdd_1) = %q, %v", w, err)
|
||||||
|
|||||||
@@ -380,6 +380,23 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
if(!d.mount_path) return '';
|
if(!d.mount_path) return '';
|
||||||
return registered[d.mount_path] ? '<span class="badge badge-ok">Regisztrálva</span>' : '<span class="badge badge-muted">Nem regisztrált</span>';
|
return registered[d.mount_path] ? '<span class="badge badge-ok">Regisztrálva</span>' : '<span class="badge badge-muted">Nem regisztrált</span>';
|
||||||
}
|
}
|
||||||
|
// appBackingTag marks the storages that actually hold deployed apps: the internal SSD (app
|
||||||
|
// databases + Docker) and the external user-data drives (large app files). Keyed on the agent's
|
||||||
|
// authoritative role/type — pure presentation, no agent contract change.
|
||||||
|
function appBackingTag(d){
|
||||||
|
if(d.type==='lvmthin') return '<span class="badge badge-info">Alkalmazás-rendszer</span>';
|
||||||
|
if(d.role==='user-data') return '<span class="badge badge-info">Alkalmazás-adatok</span>';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// purposeDesc explains, in plain Hungarian, what each storage is for — so the "which one do the
|
||||||
|
// apps use?" question is answered per-card. Keyed on type first, then role.
|
||||||
|
function purposeDesc(d){
|
||||||
|
if(d.type==='lvmthin') return 'Belső SSD — a szerver rendszere, a Docker és a telepített alkalmazások adatbázisai itt találhatók.';
|
||||||
|
if(d.type==='local'||d.type==='dir') return 'Host tárhely — rendszer-sablonok, ISO-k, host szintű mentések. Nem tárol alkalmazásadatot.';
|
||||||
|
if(d.type==='pbs'||d.role==='backup') return 'A biztonsági mentések tárhelye.';
|
||||||
|
if(d.role==='user-data') return 'Külső adattároló — a telepített alkalmazások nagy méretű fájljai (média, dokumentumok) ide kerülnek.';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
function capBar(d){
|
function capBar(d){
|
||||||
if(!d.total_bytes || d.total_bytes<=0) return '';
|
if(!d.total_bytes || d.total_bytes<=0) return '';
|
||||||
var pct = d.used_fraction ? d.used_fraction*100 : (d.used_bytes/d.total_bytes*100);
|
var pct = d.used_fraction ? d.used_fraction*100 : (d.used_bytes/d.total_bytes*100);
|
||||||
@@ -387,11 +404,17 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
return '<div class="drive-cap"><div class="system-bar"><div class="system-bar-fill '+usageColorClass(pct)+'" style="width:'+pct.toFixed(1)+'%"></div></div>'
|
return '<div class="drive-cap"><div class="system-bar"><div class="system-bar-fill '+usageColorClass(pct)+'" style="width:'+pct.toFixed(1)+'%"></div></div>'
|
||||||
+'<div class="drive-cap-label">'+hum(d.used_bytes)+' / '+hum(d.total_bytes)+' ('+pct.toFixed(0)+'%)</div></div>';
|
+'<div class="drive-cap-label">'+hum(d.used_bytes)+' / '+hum(d.total_bytes)+' ('+pct.toFixed(0)+'%)</div></div>';
|
||||||
}
|
}
|
||||||
function actions(d){
|
function actions(d, registered){
|
||||||
// Destructive controls ONLY for user-data drives that are mounted under /mnt. System/backup get none.
|
// Destructive controls ONLY for user-data drives that are mounted under /mnt. System/backup get none.
|
||||||
if(d.role!=='user-data' || !d.mount_path || d.mount_path.indexOf('/mnt/')!==0) return '';
|
if(d.role!=='user-data' || !d.mount_path || d.mount_path.indexOf('/mnt/')!==0) return '';
|
||||||
var dev = esc(d.backing_device||''), mp = esc(d.mount_path);
|
var dev = esc(d.backing_device||''), mp = esc(d.mount_path);
|
||||||
var btns = '<button class="btn btn-xs btn-danger-outline" onclick="confirmEject(\''+mp+'\')">Leválasztás</button>';
|
var btns = '';
|
||||||
|
// A mounted-but-unregistered user-data drive: the natural intent is to USE it → Regisztrálás is
|
||||||
|
// the PRIMARY action (no format, no eject). Leválasztás/Törlés stay available but secondary.
|
||||||
|
if(!registered[d.mount_path]){
|
||||||
|
btns += '<button class="btn btn-xs btn-primary" onclick="registerDrive(\''+mp+'\')">Regisztrálás</button> ';
|
||||||
|
}
|
||||||
|
btns += '<button class="btn btn-xs btn-danger-outline" onclick="confirmEject(\''+mp+'\')">Leválasztás</button>';
|
||||||
if(d.backing_device){ btns += ' <button class="btn btn-xs btn-danger-outline" onclick="confirmWipe(\''+dev+'\',\''+mp+'\')">Törlés…</button>'; }
|
if(d.backing_device){ btns += ' <button class="btn btn-xs btn-danger-outline" onclick="confirmWipe(\''+dev+'\',\''+mp+'\')">Törlés…</button>'; }
|
||||||
return '<div class="drive-actions">'+btns+'</div>';
|
return '<div class="drive-actions">'+btns+'</div>';
|
||||||
}
|
}
|
||||||
@@ -403,15 +426,19 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
var disks=(j.data&&j.data.disks)||[];
|
var disks=(j.data&&j.data.disks)||[];
|
||||||
if(disks.length===0){ box.innerHTML='<p class="form-hint">Nincs észlelt meghajtó.</p>'; return; }
|
if(disks.length===0){ box.innerHTML='<p class="form-hint">Nincs észlelt meghajtó.</p>'; return; }
|
||||||
var registered={}; (window.__registeredPaths||[]).forEach(function(p){registered[p]=true;});
|
var registered={}; (window.__registeredPaths||[]).forEach(function(p){registered[p]=true;});
|
||||||
var html='<div class="drive-list">';
|
// One-line tiering explanation so "which storage do the apps use?" is answered at a glance.
|
||||||
|
var html='<p class="drive-tiering-note">Az alkalmazások rendszere és adatbázisai a belső SSD-n (local-lvm), a nagy méretű fájljaik a külső adattárolókon tárolódnak.</p>';
|
||||||
|
html+='<div class="drive-list">';
|
||||||
disks.forEach(function(d){
|
disks.forEach(function(d){
|
||||||
var sub = esc(d.type)+' · '+esc(d.backing_device||'—')+(d.mount_path?' · '+esc(d.mount_path):'');
|
var sub = esc(d.type)+' · '+esc(d.backing_device||'—')+(d.mount_path?' · '+esc(d.mount_path):'');
|
||||||
var badges = roleBadge(d.role)+classBadge(d)+dataBadge(d)+regBadge(d,registered);
|
var badges = roleBadge(d.role)+appBackingTag(d)+classBadge(d)+dataBadge(d)+regBadge(d,registered);
|
||||||
|
var purpose = purposeDesc(d);
|
||||||
html+='<div class="drive-card role-'+esc(d.role||'system')+'">'
|
html+='<div class="drive-card role-'+esc(d.role||'system')+'">'
|
||||||
+'<div class="drive-card-top"><div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div>'
|
+'<div class="drive-card-top"><div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div>'
|
||||||
+'<div class="drive-badges">'+badges+'</div></div>'
|
+'<div class="drive-badges">'+badges+'</div></div>'
|
||||||
|
+(purpose?'<div class="drive-purpose">'+esc(purpose)+'</div>':'')
|
||||||
+capBar(d)
|
+capBar(d)
|
||||||
+actions(d)
|
+actions(d,registered)
|
||||||
+'</div>';
|
+'</div>';
|
||||||
});
|
});
|
||||||
html+='</div>';
|
html+='</div>';
|
||||||
@@ -445,6 +472,15 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
document.getElementById('confirm-go').onclick=opts.onConfirm;
|
document.getElementById('confirm-go').onclick=opts.onConfirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registerDrive records an already-mounted, unregistered user-data drive into the StoragePath
|
||||||
|
// registry (no format, no eject) — makes the existing mount usable (schedulable + FileBrowser sync).
|
||||||
|
window.registerDrive=async function(where){
|
||||||
|
try{
|
||||||
|
var r=await fetch('/api/storage/register',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({where:where})});
|
||||||
|
var j=await r.json(); if(!j.ok){ alert('Regisztráció sikertelen: '+(j.error||'')); return; }
|
||||||
|
location.reload();
|
||||||
|
}catch(e){ alert('Hiba: '+e.message); }
|
||||||
|
};
|
||||||
window.confirmEject=function(where){
|
window.confirmEject=function(where){
|
||||||
var name=where.replace(/^\/mnt\//,'');
|
var name=where.replace(/^\/mnt\//,'');
|
||||||
openConfirm({title:'Meghajtó leválasztása', mount:where, mountName:name,
|
openConfirm({title:'Meghajtó leválasztása', mount:where, mountName:name,
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ async function loadDisks(){
|
|||||||
var r = await fetch('/api/disks'); var j = await r.json();
|
var r = await fetch('/api/disks'); var j = await r.json();
|
||||||
if(!j.ok){ throw new Error(j.error||'Hiba'); }
|
if(!j.ok){ throw new Error(j.error||'Hiba'); }
|
||||||
var disks = (j.data&&j.data.disks)||[];
|
var disks = (j.data&&j.data.disks)||[];
|
||||||
// Only USER-DATA drives with a block device are valid init (format) targets.
|
// Only USER-DATA drives with a block device that are NOT already mounted are valid init (format)
|
||||||
var formattable = disks.filter(function(d){ return d.backing_device!=="" && d.role==='user-data'; });
|
// targets — a mounted drive must be ejected (Leválasztás) first, like the attach wizard. This
|
||||||
if(formattable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs formázható felhasználói adatmeghajtó. (A rendszer- és biztonsági-mentés meghajtók védettek.)</p>'; return; }
|
// keeps an already-managed drive (e.g. felhom-usb) from showing up as an "initialize" candidate.
|
||||||
|
var formattable = disks.filter(function(d){ return d.backing_device!=="" && d.role==='user-data' && !d.mount_path; });
|
||||||
|
if(formattable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs formázható felhasználói adatmeghajtó. (A csatlakoztatott meghajtókat előbb le kell választani; a rendszer- és biztonsági-mentés meghajtók védettek.)</p>'; return; }
|
||||||
var html='<div class="drive-list">';
|
var html='<div class="drive-list">';
|
||||||
formattable.forEach(function(d,i){
|
formattable.forEach(function(d,i){
|
||||||
var sub = esc(d.type)+' · '+esc(d.backing_device)+(d.mount_path?' · '+esc(d.mount_path):'');
|
var sub = esc(d.type)+' · '+esc(d.backing_device)+(d.mount_path?' · '+esc(d.mount_path):'');
|
||||||
|
|||||||
@@ -3124,6 +3124,14 @@ a.stat-card:hover {
|
|||||||
.badge-ok { background: rgba(35, 134, 54, 0.18); color: #3fb950; }
|
.badge-ok { background: rgba(35, 134, 54, 0.18); color: #3fb950; }
|
||||||
.badge-lock { background: rgba(210, 153, 34, 0.18); color: var(--yellow); }
|
.badge-lock { background: rgba(210, 153, 34, 0.18); color: var(--yellow); }
|
||||||
.badge-muted { background: rgba(110, 118, 129, 0.18); color: var(--text-muted); }
|
.badge-muted { background: rgba(110, 118, 129, 0.18); color: var(--text-muted); }
|
||||||
|
.badge-info { background: rgba(0, 136, 204, 0.18); color: var(--accent-light); }
|
||||||
|
/* Per-card storage purpose description + the tiering one-liner above the drive list. */
|
||||||
|
.drive-purpose { font-size: .8rem; color: var(--text-secondary); line-height: 1.4; }
|
||||||
|
.drive-tiering-note {
|
||||||
|
font-size: .8rem; color: var(--text-secondary); line-height: 1.4;
|
||||||
|
margin: 0 0 .75rem; padding: .5rem .75rem;
|
||||||
|
background: rgba(0, 136, 204, 0.06); border-left: 3px solid var(--accent-blue); border-radius: 6px;
|
||||||
|
}
|
||||||
.badge .lock-ico { margin-right: .25rem; }
|
.badge .lock-ico { margin-right: .25rem; }
|
||||||
span.mono, .mono { font-family: 'JetBrains Mono', monospace; }
|
span.mono, .mono { font-family: 'JetBrains Mono', monospace; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user