Files
felhom-controller/controller/internal/web/templates/storage_init.html
T
admin 0025a3b090 v0.141.0: F6 initialize-to-usable (detached crash-safe init job + status poll) + F7 Vissza back-routes
- F6: POST /api/storage/init runs format→mount→register as a DETACHED single-flight job (context.Background, netAddState shape) the wizard polls via GET /api/storage/init/status; 3-step progress; register-last marker-last crash-safety. Fixes the client-disconnect-aborts-mount bug. No agent change (chain reaches FileBrowser sync = controller-only). Red-proof TestStorageInit_DetachedSurvivesClientDisconnect.
- F7: storage_init/attach Vissza → /storage (was /settings). Test TestStorageWizardBackAnchors_PointToStorage.
2026-07-17 09:20:59 +02:00

202 lines
12 KiB
HTML

{{define "storage_init"}}
{{template "layout_start" .}}
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/storage" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Új meghajtó inicializálása</h2>
</div>
</div>
<div class="settings-card">
<h3>1. Meghajtó kiválasztása</h3>
<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>
<div class="settings-card" id="cfg-card" style="display:none">
<h3>2. Konfiguráció</h3>
<form id="init-form" onsubmit="return submitInit(event)">
<div class="form-group">
<label>Kiválasztott eszköz</label>
<span class="settings-value mono" id="sel-device"></span>
</div>
<div class="form-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>
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1"
pattern="[a-zA-Z0-9_-]+" required style="max-width:180px">
</div>
<span class="form-hint">A meghajtó a /mnt/&lt;név&gt; útvonalra kerül.</span>
</div>
<div class="form-group">
<label for="fstype">Fájlrendszer</label>
<select id="fstype" class="form-control" style="max-width:180px">
<option value="ext4" selected>ext4</option>
<option value="xfs">xfs</option>
</select>
</div>
<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">
</div>
<label class="toggle" style="margin-bottom:1.25rem">
<input type="checkbox" id="set-default" checked>
<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">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> 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>
<a href="/settings" class="btn btn-outline">Mégsem</a>
</div>
<div id="init-result" style="margin-top:1rem"></div>
</form>
</div>
<script>
var selDevice = "", selDataBearing = false;
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function dataBadge(d){
return d.data_bearing
? '<span class="badge badge-error">Adatot tartalmaz — a formázás törli</span>'
: '<span class="badge badge-ok">Üres — formázható</span>';
}
function fmtSize(b){
if(!b) return '';
var g=b/1073741824;
return g>=1 ? g.toFixed(g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
}
async function loadDisks(){
try{
// Impl-2b: the raw-device scan (agent GET /disks/candidates via the controller proxy). The agent's
// unclaimed-disk filter already excludes OS/enrolled/claimed disks (fail-safe), so we trust the list
// as-is — no client-side filtering. `initialize` = every unclaimed disk (blank or data-bearing).
var r = await fetch('/api/disks/candidates'); var j = await r.json();
if(!j.ok){ throw new Error(j.error||'Hiba'); }
var cands = (j.data&&j.data.initialize)||[];
if(cands.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">Nincs elérhető meghajtó az inicializáláshoz. Csatlakoztasson egy új adathordozót — a rendszer-, a biztonsági mentés- és a már használatban lévő meghajtók itt nem jelennek meg.</p>'; return; }
var html='<div class="drive-list">';
cands.forEach(function(d,i){
var title = d.model ? esc(d.model) : esc(d.device);
var parts = [esc(d.device)];
if(d.size_bytes) parts.push(fmtSize(d.size_bytes));
if(d.fstype) parts.push('jelenlegi: '+esc(d.fstype));
var sub = parts.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(d.device)+'" data-db="'+(d.data_bearing?'1':'0')+'" data-i="'+i+'" 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">'+dataBadge(d)+'</div></div></label>';
});
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; }
}
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 STARTS the detached init job (F6). confirmed/durableId carry the customer's wipe
// confirmation (user-data). The chain (format → mount → register) now runs server-side off the
// request context and is followed via pollInitStatus — a closed tab no longer aborts it.
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()};
}
// stepper renders the 3-step progress (Formázás → Csatolás → Regisztrálás) for the running phase.
function stepper(phase){
var steps=['Formázás','Csatolás','Regisztrálás'];
var order={formatting:0,mounting:1,registering:2};
var cur=order[phase]!=null?order[phase]:0;
var rows=steps.map(function(lbl,i){
var state=i<cur?'kész':(i===cur?'folyamatban…':'vár');
var style=i<cur?'color:var(--accent-ok,#3a9a4a)':(i===cur?'font-weight:600':'opacity:.5');
return '<div style="'+style+'">'+(i+1)+'. '+esc(lbl)+' — '+state+'</div>';
});
return '<div class="init-steps" style="display:flex;flex-direction:column;gap:.25rem">'+rows.join('')+'</div>';
}
function sleep(ms){ return new Promise(function(r){ setTimeout(r,ms); }); }
// pollInitStatus follows the detached job to a terminal state (F6): 3-step progress while running,
// the confirm/refuse verdicts, or success/failure.
async function pollInitStatus(out, btn){
for(;;){
await sleep(1000);
var r; try{ r=await fetch('/api/storage/init/status'); }catch(e){ continue; }
var j; try{ j=await r.json(); }catch(e){ continue; }
if(!j.ok){ out.innerHTML='<div class="alert alert-error">Az állapot lekérdezése sikertelen.</div>'; btn.disabled=false; return; }
var d=j.data||{}; var ph=d.phase;
if(ph==='formatting'||ph==='mounting'||ph==='registering'){ out.innerHTML=stepper(ph); continue; }
if(ph==='idle'){ continue; } // slot not yet populated
if(ph==='needs_confirmation'){ renderConfirm(d.durable_id, out, btn); return; }
if(ph==='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).'
+(d.opsign?('<pre class="mono" style="white-space:pre-wrap;background:var(--bg-0);padding:.75rem;border-radius:6px;margin-top:.5rem">'+esc(d.opsign)+'</pre>'):'')+'</div>';
btn.disabled=false; return;
}
if(ph==='done'){ finishInit(d.where, out); return; }
if(ph==='failed'){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(d.error||'')+'</div>'; btn.disabled=false; return; }
}
}
async function submitInit(ev){
ev.preventDefault();
var btn=document.getElementById('init-btn'); var out=document.getElementById('init-result');
btn.disabled=true; out.innerHTML=stepper('formatting');
try{
var res=await postInit(false, "");
if(res.status===409){ out.innerHTML='<div class="alert alert-warning">'+esc((res.j&&res.j.error)||'Már folyamatban van egy inicializálás.')+'</div>'; btn.disabled=false; return false; }
if(!res.j.ok){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(res.j.error||'')+'</div>'; btn.disabled=false; return false; }
await pollInitStatus(out, btn);
}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 indítása…</p>';
try{
var res2=await postInit(true, durableId);
if(res2.status===409){ cr.innerHTML='<div class="alert alert-warning">'+esc((res2.j&&res2.j.error)||'')+'</div>'; return; }
if(!res2.j.ok){ cr.innerHTML='<div class="alert alert-error">Hiba: '+esc(res2.j.error||'')+'</div>'; return; }
out.innerHTML=stepper('formatting');
await pollInitStatus(out, btn);
}catch(e){ cr.innerHTML='<div class="alert alert-error">Hiba: '+esc(e.message)+'</div>'; }
};
}
function finishInit(where, out){
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen inicializálva és regisztrálva: <strong class="mono">'+esc(where)+'</strong>. <a href="/storage">Vissza a tárhelyhez →</a></div>';
}
loadDisks();
</script>
{{template "layout_end" .}}
{{end}}