R-280: attach list from mounted-but-unregistered filesystems; two-clicks promise made conditional
gates / gates (push) Successful in 17s

After a reinstall the data drive could not be re-attached through any dashboard
route: both candidate lists came from the agent's unclaimed-disk scan, and the
rebuilt box's drives are claimed. The restore page said it was two clicks while
pointing at an empty picker.

The attach list now also carries the controller's own mounted-but-unregistered
filesystems. initialize is untouched, so the format wizard's system/backup
protection is unchanged. The 'two clicks' sentence is conditional on the picker
being non-empty, and says something true and actionable when it is not.
This commit is contained in:
2026-08-10 13:41:32 +02:00
parent c732fe1283
commit b762a37097
9 changed files with 600 additions and 12 deletions
@@ -23,7 +23,9 @@
<label>Kiválasztott eszköz</label>
<span class="settings-value mono" id="sel-device"></span>
</div>
<div class="form-group">
<!-- R-280: an already-mounted store has no mount name to choose — it is already at its path,
and the action is to register that path. The group is hidden for those. -->
<div class="form-group" id="mount-name-group">
<label for="mount-name">Csatlakoztatási név <span class="required">*</span></label>
<div style="display:flex;align-items:center;gap:.25rem">
<span class="mono" style="opacity:.6">/mnt/</span>
@@ -32,6 +34,10 @@
</div>
<span class="form-hint">A meghajtó a /mnt/&lt;név&gt; útvonalra kerül.</span>
</div>
<p class="form-hint" id="mounted-note" style="display:none">
Ez a meghajtó már csatlakoztatva van, csak a gép nem tartja nyilván. A „Csatolás" a
meglévő helyén veszi nyilvántartásba — <strong>semmi nem törlődik</strong>.
</p>
<div class="form-group">
<label for="storage-label">Megnevezés</label>
<input type="text" id="storage-label" class="form-control" placeholder="Külső HDD 1TB" maxlength="50">
@@ -49,7 +55,7 @@
</div>
<script>
var selDevice = "", selFSType = "";
var selDevice = "", selFSType = "", selMounted = false;
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function fmtSize(b){
if(!b) return '';
@@ -74,11 +80,15 @@ async function loadDisks(){
if(d.fstype) parts.push(esc(d.fstype));
var sub = parts.join(' · ');
// Attach the FS-bearing node (mount_source, e.g. /dev/sdd1); the agent resolves its UUID.
// R-280: for an already-mounted store mount_source is its MOUNTPOINT (/mnt/sys_drive) and the
// action is register-in-place, so the card is labelled by the path the customer will see.
var dev = d.mount_source || d.device;
var mounted = !!d.already_mounted;
if(mounted){ title = esc(d.mount_source); sub = [esc(d.device), esc(d.fstype)].filter(Boolean).join(' · '); }
html+='<label class="drive-card role-user-data is-selectable" id="dc-'+i+'">'
+'<div class="drive-card-top"><div class="drive-select"><input type="radio" name="disk" value="'+esc(dev)+'" data-fs="'+esc(d.fstype)+'" data-i="'+i+'" onchange="pickDisk(this)">'
+'<div class="drive-card-top"><div class="drive-select"><input type="radio" name="disk" value="'+esc(dev)+'" data-fs="'+esc(d.fstype)+'" data-i="'+i+'" data-mounted="'+(mounted?'1':'')+'" onchange="pickDisk(this)">'
+'<div class="drive-id"><span class="drive-name">'+title+'</span><span class="drive-sub">'+sub+'</span></div></div>'
+'<div class="drive-badges"><span class="badge badge-ok">Fájlrendszer: '+esc(d.fstype)+'</span></div></div></label>';
+'<div class="drive-badges">'+(mounted?'<span class="badge badge-ok">Már csatlakoztatva</span>':'<span class="badge badge-ok">Fájlrendszer: '+esc(d.fstype)+'</span>')+'</div></div></label>';
});
html+='</div>';
document.getElementById('disk-list').innerHTML=html;
@@ -87,7 +97,14 @@ async function loadDisks(){
function pickDisk(radio){
selDevice=radio.value; selFSType=radio.getAttribute('data-fs')||"";
selMounted=!!radio.getAttribute('data-mounted');
document.getElementById('sel-device').textContent=selDevice;
// An already-mounted store keeps its own path; there is no name to pick. Dropping `required` too,
// or the hidden empty field blocks form submission with no visible cause.
var mng=document.getElementById('mount-name-group'), mni=document.getElementById('mount-name');
mng.style.display = selMounted ? 'none' : '';
mni.required = !selMounted;
document.getElementById('mounted-note').style.display = selMounted ? '' : 'none';
document.querySelectorAll('.drive-card').forEach(function(c){c.classList.remove('is-picked');});
var card=document.getElementById('dc-'+radio.getAttribute('data-i')); if(card) card.classList.add('is-picked');
document.getElementById('cfg-card').style.display='block';
@@ -99,12 +116,22 @@ async function submitAttach(ev){
var btn=document.getElementById('attach-btn'); var out=document.getElementById('attach-result');
btn.disabled=true; out.innerHTML='<p class="form-hint">Csatlakoztatás folyamatban…</p>';
try{
var body={device:selDevice, fstype:selFSType, mount_name:document.getElementById('mount-name').value,
label:document.getElementById('storage-label').value, set_default:document.getElementById('set-default').checked};
var r=await fetch('/api/storage/attach',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)});
// R-280: an already-mounted store is REGISTERED in place. Sending it to /api/storage/attach would
// ask the agent to mount an in-guest path as if it were a raw device.
var url, body;
if(selMounted){
url='/api/storage/register-mounted';
body={path:selDevice, label:document.getElementById('storage-label').value,
set_default:document.getElementById('set-default').checked};
}else{
url='/api/storage/attach';
body={device:selDevice, fstype:selFSType, mount_name:document.getElementById('mount-name').value,
label:document.getElementById('storage-label').value, set_default:document.getElementById('set-default').checked};
}
var r=await fetch(url,{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)});
var j=await r.json();
if(!j.ok){ throw new Error(j.error||'Hiba'); }
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen csatolva és regisztrálva: <strong class="mono">'+(j.data.where||'')+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen '+(selMounted?'nyilvántartásba véve':'csatolva és regisztrálva')+': <strong class="mono">'+(j.data.where||'')+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
}catch(e){ out.innerHTML='<div class="alert alert-error">Hiba: '+e.message+'</div>'; btn.disabled=false; }
return false;
}