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.
This commit is contained in:
2026-07-17 09:20:59 +02:00
parent 5be2449267
commit 0025a3b090
7 changed files with 365 additions and 38 deletions
@@ -3,7 +3,7 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
<a href="/storage" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Új meghajtó inicializálása</h2>
</div>
</div>
@@ -111,7 +111,9 @@ function pickDisk(radio){
document.getElementById('cfg-card').scrollIntoView({behavior:'smooth'});
}
// postInit runs the init POST. confirmed/durableId carry the customer's wipe confirmation (user-data).
// 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,
@@ -120,24 +122,52 @@ async function postInit(confirmed, durableId){
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='<p class="form-hint">Formázás és csatlakoztatás folyamatban…</p>';
btn.disabled=true; out.innerHTML=stepper('formatting');
try{
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-0);padding:.75rem;border-radius:6px;margin-top:.5rem">'+esc(res.j.data.opsign)+'</pre>'):'')+'</div>';
btn.disabled=false; return false;
}
finishInit(res.j, out);
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;
}
@@ -150,18 +180,19 @@ function renderConfirm(durableId, out, btn){
+'<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>';
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; }
finishInit(res2.j, out);
out.innerHTML=stepper('formatting');
await pollInitStatus(out, btn);
}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>';
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>