v0.44.0: role-aware drive management — protected lockout + customer type-to-confirm wipe + drive-list restyle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 21:44:50 +02:00
parent 2c32c821fe
commit 12064dcd88
13 changed files with 696 additions and 182 deletions
+98 -22
View File
@@ -354,46 +354,122 @@ function pollUntilBack() {
<div style="margin-top:1.5rem">
<h4 style="margin-bottom:.25rem">Meghajtók (ügynök nézet)</h4>
<p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete (a tárolás végrehajtása az ügynöké).</p>
<p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete. A meghajtó <strong>szerepkörét</strong> az ügynök saját vizsgálattal állapítja meg: a rendszer- és biztonsági-mentés meghajtók védettek (csak operátori aláírással módosíthatók), a felhasználói adatmeghajtókat Ön kezeli.</p>
<div id="agent-disks">Betöltés…</div>
</div>
<div id="confirm-root"></div>
<script>
window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}{{end}}];
(function(){
function badge(d){
if(d.backing_device===""){ return ''; }
return d.data_bearing
? '<span class="badge badge-error" title="'+(d.data_reason||'')+'">Adatot tartalmaz</span>'
: '<span class="badge badge-ok">Üres</span>';
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function hum(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB','TB'],i=0,v=b; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
function usageColorClass(p){ if(p>=85) return 'system-bar-red'; if(p>=70) return 'system-bar-yellow'; return 'system-bar-green'; }
function classBadge(d){
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
return '';
}
function roleBadge(role){
if(role==='system') return '<span class="badge badge-lock"><span class="lock-ico">🔒</span>Rendszer</span>';
if(role==='backup') return '<span class="badge badge-lock"><span class="lock-ico">🔒</span>Biztonsági mentés — védett</span>';
if(role==='user-data') return '<span class="badge badge-ok">Felhasználói adat</span>';
return '<span class="badge badge-lock"><span class="lock-ico">🔒</span>Védett</span>';
}
function dataBadge(d){ return d.data_bearing ? '<span class="badge badge-error" title="'+esc(d.data_reason)+'">Adatot tartalmaz</span>' : ''; }
function regBadge(d, registered){
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>';
}
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);
pct = Math.max(0, Math.min(100, pct));
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){
// 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>';
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>';
}
function reg(d, registered){ return registered[d.mount_path] ? '<span class="badge badge-ok">Regisztrálva</span>' : (d.mount_path?'<span class="badge">Nem regisztrált</span>':''); }
async function load(){
var box=document.getElementById('agent-disks'); if(!box) return;
try{
var r=await fetch('/api/disks'); var j=await r.json();
if(!j.ok){ box.innerHTML='<p class="form-hint">'+(j.error||'Nem elérhető')+'</p>'; return; }
if(!j.ok){ box.innerHTML='<p class="form-hint">'+esc(j.error||'Nem elérhető')+'</p>'; return; }
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='<table class="data-table"><thead><tr><th>Tároló</th><th>Típus</th><th>Eszköz</th><th>Csatolás</th><th>Osztály</th><th>Adat</th><th>Reg.</th><th></th></tr></thead><tbody>';
var html='<div class="drive-list">';
disks.forEach(function(d){
var ej = (d.mount_path && d.mount_path.indexOf('/mnt/')===0) ? '<button class="btn btn-xs btn-danger-outline" onclick="ejectDisk(\''+d.mount_path+'\')">Leválasztás</button>' : '';
html+='<tr><td>'+d.name+'</td><td>'+d.type+'</td><td class="mono">'+(d.backing_device||'—')+'</td><td class="mono">'+(d.mount_path||'—')+'</td><td>'+(d.class||'—')+'</td><td>'+badge(d)+'</td><td>'+reg(d,registered)+'</td><td>'+ej+'</td></tr>';
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);
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>'
+capBar(d)
+actions(d)
+'</div>';
});
html+='</tbody></table>';
html+='</div>';
box.innerHTML=html;
}catch(e){ box.innerHTML='<p class="form-hint">Hiba: '+e.message+'</p>'; }
}catch(e){ box.innerHTML='<p class="form-hint">Hiba: '+esc(e.message)+'</p>'; }
}
window.ejectDisk=async function(where){
if(!confirm('Leválasztja a(z) '+where+' meghajtót? Az adatok megmaradnak, de az ott lévő alkalmazások elveszítik a tárhelyet.')) return;
// ---- type-to-confirm modal (destructive user-data actions) ----
function closeModal(){ document.getElementById('confirm-root').innerHTML=''; }
window.__closeConfirm=closeModal;
async function openConfirm(opts){
// opts: {title, mount, mountName, danger, onConfirm}
var apps=[];
try{
var r=await fetch('/api/storage/eject',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({where:where})});
var j=await r.json();
if(!j.ok){ alert('Hiba: '+(j.error||'')); return; }
var dep=(j.data&&j.data.dependent_guests)||[];
if(dep.length>0){ alert('Leválasztva. Figyelem: '+dep.length+' vendég (VMID: '+dep.join(', ')+') függött ettől a tárhelytől.'); }
location.reload();
}catch(e){ alert('Hiba: '+e.message); }
var r=await fetch('/api/storage/impact?where='+encodeURIComponent(opts.mount));
var j=await r.json(); if(j.ok && j.data && j.data.apps) apps=j.data.apps;
}catch(e){}
var appsHtml = apps.length
? '<p>A művelet után a következő alkalmazások <strong>nem fognak működni</strong>:</p><ul class="confirm-apps">'+apps.map(function(a){return '<li>'+esc(a)+'</li>';}).join('')+'</ul>'
: '<p class="form-hint">Ehhez a meghajtóhoz jelenleg nincs telepített alkalmazás rendelve.</p>';
var root=document.getElementById('confirm-root');
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)__closeConfirm()"><div class="confirm-box">'
+'<h3>'+esc(opts.title)+'</h3>'
+'<div class="alert alert-warning">'+esc(opts.danger)+'</div>'
+appsHtml
+'<div class="confirm-input"><label>Megerősítéshez írja be a csatlakoztatási nevet: <strong class="mono">'+esc(opts.mountName)+'</strong></label>'
+'<input type="text" id="confirm-type" class="form-control" autocomplete="off" placeholder="'+esc(opts.mountName)+'" oninput="document.getElementById(\'confirm-go\').disabled=(this.value!==\''+esc(opts.mountName)+'\')"></div>'
+'<div class="form-actions"><button id="confirm-go" class="btn btn-danger-outline" disabled>Megerősítés</button>'
+'<button class="btn btn-outline" onclick="__closeConfirm()">Mégsem</button></div>'
+'<div id="confirm-result" style="margin-top:.6rem"></div></div></div>';
document.getElementById('confirm-go').onclick=opts.onConfirm;
}
window.confirmEject=function(where){
var name=where.replace(/^\/mnt\//,'');
openConfirm({title:'Meghajtó leválasztása', mount:where, mountName:name,
danger:'A meghajtó leválasztásra kerül. Az adatok megmaradnak, de az ott tárolt alkalmazások elvesztik a tárhelyüket, amíg újra nem csatolja.',
onConfirm:async function(){
var out=document.getElementById('confirm-result'); out.innerHTML='<p class="form-hint">Leválasztás folyamatban…</p>';
try{
var r=await fetch('/api/storage/eject',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({where:where})});
var j=await r.json(); if(!j.ok){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(j.error||'')+'</div>'; return; }
location.reload();
}catch(e){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(e.message)+'</div>'; }
}});
};
window.confirmWipe=function(device, where){
var name=where.replace(/^\/mnt\//,'');
openConfirm({title:'Meghajtó törlése (formázás)', mount:where, mountName:name,
danger:'FIGYELEM: a meghajtón lévő ÖSSZES ADAT véglegesen törlődik (formázás). Ez nem vonható vissza.',
onConfirm:async function(){
var out=document.getElementById('confirm-result'); out.innerHTML='<p class="form-hint">Törlés folyamatban…</p>';
try{
var r=await fetch('/api/storage/wipe',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({device:device, where:where, mount_name:name})});
var j=await r.json(); if(!j.ok){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(j.error||'')+'</div>'; return; }
location.reload();
}catch(e){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(e.message)+'</div>'; }
}});
};
load();
})();
@@ -50,21 +50,30 @@
<script>
var selDevice = "", selFSType = "";
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function classBadge(d){
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
return '';
}
async function loadDisks(){
try{
var r = await fetch('/api/disks'); var j = await r.json();
if(!j.ok){ throw new Error(j.error||'Hiba'); }
var disks = (j.data&&j.data.disks)||[];
// Attachable: has a backing device, an fs-UUID identity (durable_id "uuid:…"), and isn't mounted yet.
var attachable = disks.filter(function(d){ return d.backing_device!=="" && (d.durable_id||"").indexOf("uuid:")===0 && !d.mount_path; });
if(attachable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs csatolható (fájlrendszerrel rendelkező, még nem csatolt) meghajtó.</p>'; return; }
var html='<table class="data-table"><thead><tr><th></th><th>Tároló</th><th>Típus</th><th>Eszköz</th><th>Osztály</th></tr></thead><tbody>';
attachable.forEach(function(d){
html+='<tr><td><input type="radio" name="disk" value="'+d.backing_device+'" onchange="pickDisk(this)"></td>'
+'<td>'+d.name+'</td><td>'+d.type+'</td><td class="mono">'+d.backing_device+'</td><td>'+(d.class||'—')+'</td></tr>';
// Attachable: a user-data drive with a backing device, an fs-UUID identity, not mounted yet.
var attachable = disks.filter(function(d){ return d.backing_device!=="" && (d.durable_id||"").indexOf("uuid:")===0 && !d.mount_path && d.role==='user-data'; });
if(attachable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs csatolható (fájlrendszerrel rendelkező, még nem csatolt) felhasználói adatmeghajtó.</p>'; return; }
var html='<div class="drive-list">';
attachable.forEach(function(d,i){
var sub = esc(d.type)+' · '+esc(d.backing_device);
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(d.backing_device)+'" data-i="'+i+'" onchange="pickDisk(this)">'
+'<div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div></div>'
+'<div class="drive-badges"><span class="badge badge-ok">Felhasználói adat</span>'+classBadge(d)+'</div></div></label>';
});
html+='</tbody></table>';
html+='</div>';
document.getElementById('disk-list').innerHTML=html;
}catch(e){ var el=document.getElementById('disk-error'); el.style.display='block'; el.textContent='Meghajtók betöltése sikertelen: '+e.message; }
}
@@ -72,6 +81,8 @@ async function loadDisks(){
function pickDisk(radio){
selDevice=radio.value;
document.getElementById('sel-device').textContent=selDevice;
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';
document.getElementById('cfg-card').scrollIntoView({behavior:'smooth'});
}
@@ -10,8 +10,9 @@
<div class="settings-card">
<h3>1. Meghajtó kiválasztása</h3>
<p class="settings-card-desc">Válassza ki a formázandó meghajtót. A formázás biztonságát a host-ügynök
garantálja: <strong>adatot tartalmazó meghajtó nem formázható operátori aláírás nélkül</strong>.</p>
<p class="settings-card-desc">Válassza ki a formázandó <strong>felhasználói adatmeghajtót</strong>. Rendszer- és
biztonsági-mentés meghajtók itt nem jelennek meg — azok védettek. Ha a meghajtó adatot tartalmaz, a törlést
Önnek meg kell erősítenie.</p>
<div id="disk-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
<div id="disk-list">Betöltés…</div>
</div>
@@ -48,8 +49,8 @@
<span class="toggle-label">Beállítás alapértelmezett adattárolóként új telepítéseknél</span>
</label>
<div class="alert alert-warning" id="warn-databearing" style="display:none;margin-bottom:1rem">
⚠️ A kiválasztott meghajtó <strong>adatot tartalmaz</strong>. A formázás védelmi okból csak
operátori aláírással hajtható végre — a rendszer megmutatja a szükséges parancsot.
⚠️ A kiválasztott meghajtó <strong>adatot tartalmaz</strong>. Az inicializálás (formázás) törli a rajta lévő
összes adatot — a folytatáshoz meg kell erősítenie.
</div>
<div class="form-actions" style="gap:.75rem">
<button type="submit" class="btn btn-danger-outline" id="init-btn">Inicializálás indítása</button>
@@ -61,28 +62,36 @@
<script>
var selDevice = "", selDataBearing = false;
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function badge(d){
if(d.backing_device===""){ return '<span class="badge">—</span>'; }
function dataBadge(d){
return d.data_bearing
? '<span class="badge badge-error" title="'+(d.data_reason||'')+'">Adatot tartalmaz</span>'
? '<span class="badge badge-error" title="'+esc(d.data_reason)+'">Adatot tartalmaz</span>'
: '<span class="badge badge-ok">Üres — formázható</span>';
}
function classBadge(d){
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
return '';
}
async function loadDisks(){
try{
var r = await fetch('/api/disks'); var j = await r.json();
if(!j.ok){ throw new Error(j.error||'Hiba'); }
var disks = (j.data&&j.data.disks)||[];
var formattable = disks.filter(function(d){return d.backing_device!=="";});
if(formattable.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs formázható (blokkeszközzel rendelkező) meghajtó.</p>'; return; }
var html='<table class="data-table"><thead><tr><th></th><th>Tároló</th><th>Típus</th><th>Eszköz</th><th>Állapot</th><th>Osztály</th><th>Adat</th></tr></thead><tbody>';
// Only USER-DATA drives with a block device are valid init (format) targets.
var formattable = disks.filter(function(d){ return d.backing_device!=="" && d.role==='user-data'; });
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; }
var html='<div class="drive-list">';
formattable.forEach(function(d,i){
html+='<tr><td><input type="radio" name="disk" value="'+d.backing_device+'" data-db="'+(d.data_bearing?'1':'0')+'" onchange="pickDisk(this)"></td>'
+'<td>'+d.name+'</td><td>'+d.type+'</td><td class="mono">'+d.backing_device+'</td>'
+'<td>'+(d.state==='attached'?'csatlakoztatva':d.state)+'</td><td>'+(d.class||'')+'</td><td>'+badge(d)+'</td></tr>';
var sub = esc(d.type)+' · '+esc(d.backing_device)+(d.mount_path?' · '+esc(d.mount_path):'');
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(d.backing_device)+'" data-db="'+(d.data_bearing?'1':'0')+'" data-i="'+i+'" onchange="pickDisk(this)">'
+'<div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span><span class="drive-sub">'+sub+'</span></div></div>'
+'<div class="drive-badges"><span class="badge badge-ok">Felhasználói adat</span>'+classBadge(d)+dataBadge(d)+'</div></div></label>';
});
html+='</tbody></table>';
html+='</div>';
document.getElementById('disk-list').innerHTML=html;
}catch(e){ var el=document.getElementById('disk-error'); el.style.display='block'; el.textContent='Meghajtók betöltése sikertelen: '+e.message; }
}
@@ -91,33 +100,64 @@ function pickDisk(radio){
selDevice=radio.value; selDataBearing=radio.getAttribute('data-db')==='1';
document.getElementById('sel-device').textContent=selDevice;
document.getElementById('warn-databearing').style.display=selDataBearing?'block':'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';
document.getElementById('cfg-card').scrollIntoView({behavior:'smooth'});
}
// postInit runs the init POST. confirmed/durableId carry the customer's wipe confirmation (user-data).
async function postInit(confirmed, durableId){
var body={device:selDevice, fstype:document.getElementById('fstype').value,
mount_name:document.getElementById('mount-name').value, label:document.getElementById('storage-label').value,
set_default:document.getElementById('set-default').checked, confirmed:!!confirmed, durable_id:durableId||""};
var r=await fetch('/api/storage/init',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)});
return {status:r.status, j:await r.json()};
}
async function submitInit(ev){
ev.preventDefault();
var btn=document.getElementById('init-btn'); var out=document.getElementById('init-result');
btn.disabled=true; out.innerHTML='<p class="form-hint">Formázás és csatlakoztatás folyamatban…</p>';
try{
var body={device:selDevice, fstype:document.getElementById('fstype').value,
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/init',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)});
var j=await r.json();
if(r.status===409 && j.data && j.data.refused){
out.innerHTML='<div class="alert alert-warning"><strong>Operátori aláírás szükséges.</strong><br>'
+'A meghajtó adatot tartalmaz, ezért a formázás védelmi okból nem hajtható végre automatikusan'
+(j.data.reason?(' ('+j.data.reason+')'):'')+'.<br><br>Az engedélyezéshez futtassa offline az operátor gépén:'
+'<pre class="mono" style="white-space:pre-wrap;background:var(--bg-primary);padding:.75rem;border-radius:6px;margin-top:.5rem">'+(j.data.opsign||'(nem elérhető)')+'</pre>'
+'Az aláírás után a Hub végrehajtja a műveletet; ezután térjen vissza ide.</div>';
var res=await postInit(false, "");
// USER-DATA data-bearing → the customer must confirm the wipe (type-to-confirm), then re-submit.
if(res.status===409 && res.j.data && res.j.data.needs_confirmation){
renderConfirm(res.j.data.durable_id, out, btn);
return false;
}
// SYSTEM/BACKUP (shouldn't reach here — filtered out — but surface the opsign if it does).
if(res.status===409 && res.j.data && res.j.data.refused){
out.innerHTML='<div class="alert alert-warning"><strong>Operátori aláírás szükséges.</strong> Ez a meghajtó védett (rendszer/biztonsági mentés).'
+(res.j.data.opsign?('<pre class="mono" style="white-space:pre-wrap;background:var(--bg-primary);padding:.75rem;border-radius:6px;margin-top:.5rem">'+esc(res.j.data.opsign)+'</pre>'):'')+'</div>';
btn.disabled=false; return false;
}
if(!j.ok){ throw new Error(j.error||'Hiba'); }
out.innerHTML='<div class="alert alert-success">✅ A meghajtó sikeresen inicializálva é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; }
finishInit(res.j, out);
}catch(e){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(e.message)+'</div>'; btn.disabled=false; }
return false;
}
function renderConfirm(durableId, out, btn){
var name=document.getElementById('mount-name').value;
out.innerHTML='<div class="alert alert-warning">⚠️ A meghajtó adatot tartalmaz. A formázás <strong>véglegesen törli</strong> a rajta lévő összes adatot.</div>'
+'<div class="confirm-input"><label>Megerősítéshez írja be a csatlakoztatási nevet: <strong class="mono">'+esc(name)+'</strong></label>'
+'<input type="text" id="init-confirm-type" class="form-control" autocomplete="off" oninput="document.getElementById(\'init-confirm-go\').disabled=(this.value!==\''+esc(name)+'\')"></div>'
+'<div class="form-actions" style="gap:.6rem;margin-top:.75rem"><button id="init-confirm-go" class="btn btn-danger-outline" disabled>Törlés és inicializálás megerősítése</button></div>'
+'<div id="init-confirm-result" style="margin-top:.6rem"></div>';
document.getElementById('init-confirm-go').onclick=async function(){
var cr=document.getElementById('init-confirm-result'); cr.innerHTML='<p class="form-hint">Törlés és inicializálás folyamatban…</p>';
try{
var res2=await postInit(true, durableId);
if(!res2.j.ok){ cr.innerHTML='<div class="alert alert-error">Hiba: '+esc(res2.j.error||'')+'</div>'; return; }
finishInit(res2.j, out);
}catch(e){ cr.innerHTML='<div class="alert alert-error">Hiba: '+esc(e.message)+'</div>'; }
};
}
function finishInit(j, out){
if(!j.ok){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(j.error||'')+'</div>'; document.getElementById('init-btn').disabled=false; return; }
out.innerHTML='<div class="alert alert-success">✅ A meghajtó sikeresen inicializálva és regisztrálva: <strong class="mono">'+esc(j.data&&j.data.where)+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
}
loadDisks();
</script>
@@ -3086,3 +3086,58 @@ a.stat-card:hover {
border-radius: 6px; padding: 0.2rem 0.5rem; cursor: pointer; font-size: 0.8rem;
}
.btn-danger-outline:hover { background: var(--red-bg); }
/* ============================================================================
Agent drive lists (overview + init/attach selector) — storage authz redesign.
Reuses the existing card/badge/bar tokens; no new design system.
============================================================================ */
.drive-list { display: flex; flex-direction: column; gap: .6rem; margin-top: .25rem; }
.drive-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-left: 4px solid var(--gray, #6e7681);
border-radius: 12px;
padding: .85rem 1rem;
display: flex; flex-direction: column; gap: .55rem;
}
.drive-card.role-user-data { border-left-color: var(--accent-blue); }
.drive-card.role-system,
.drive-card.role-backup { border-left-color: var(--yellow); }
.drive-card.is-selectable { cursor: pointer; transition: border-color .15s, background .15s; }
.drive-card.is-selectable:hover { border-color: var(--accent-light); }
.drive-card.is-picked { border-color: var(--accent-light); background: rgba(0, 136, 204, 0.06); }
.drive-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; }
.drive-id { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.drive-name { font-size: .95rem; font-weight: 600; color: var(--text-primary); }
.drive-sub {
font-size: .78rem; color: var(--text-muted);
font-family: 'JetBrains Mono', monospace; word-break: break-all;
}
.drive-badges { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; justify-content: flex-end; }
.drive-cap { display: flex; flex-direction: column; gap: .3rem; }
.drive-cap .system-bar { height: 8px; }
.drive-cap-label { font-size: .75rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
.drive-actions { display: flex; flex-wrap: wrap; gap: .4rem; }
.drive-select { display: flex; align-items: center; gap: .5rem; }
/* badges missing from the global sheet */
.badge-ok { background: rgba(35, 134, 54, 0.18); color: #3fb950; }
.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 .lock-ico { margin-right: .25rem; }
span.mono, .mono { font-family: 'JetBrains Mono', monospace; }
/* Type-to-confirm modal (destructive user-data eject/wipe) */
.confirm-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.6);
display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 1rem;
}
.confirm-box {
background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 14px;
max-width: 480px; width: 100%; padding: 1.4rem; box-shadow: 0 12px 40px rgba(0,0,0,.5);
}
.confirm-box h3 { margin: 0 0 .75rem; font-size: 1.05rem; }
.confirm-box .confirm-apps { margin: .5rem 0; padding-left: 1.1rem; }
.confirm-box .confirm-apps li { margin: .15rem 0; }
.confirm-box .confirm-input { margin: .9rem 0 .4rem; }
.confirm-box .form-actions { display: flex; gap: .6rem; margin-top: 1rem; }