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
@@ -380,6 +380,23 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
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>';
}
// 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){
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);
@@ -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>'
+'<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.
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 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>'; }
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)||[];
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 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){
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')+'">'
+'<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>'
+(purpose?'<div class="drive-purpose">'+esc(purpose)+'</div>':'')
+capBar(d)
+actions(d)
+actions(d,registered)
+'</div>';
});
html+='</div>';
@@ -445,6 +472,15 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
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){
var name=where.replace(/^\/mnt\//,'');
openConfirm({title:'Meghajtó leválasztása', mount:where, mountName:name,