Files
felhom-controller/controller/internal/web/templates/storage_init.html
T
admin 36da4089ca
gates / gates (push) Successful in 45s
v0.318.0: a part is offered for formatting only while nothing on its disk is mounted (agent 0.158.1 formattable)
The agent's root mkfs wrapper (security review of agent 0.158.0) refuses any mounted node and trusts no sibling claim; the
wizard now matches it. A free filesystem part of an in-use disk stays offered for use. Red-proved.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-16 04:05:09 +02:00

210 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">{{T "storage_init.vissza"}}</a>
<h2>{{T "storage_init.uj_meghajto_inicializalasa"}}</h2>
</div>
</div>
<div class="settings-card">
<h3>{{T "storage_init.1_meghajto_kivalasztasa"}}</h3>
<p class="settings-card-desc">{{T "storage_init.valassza_ki_a_formazando_felhasznaloi"}}</p>
<div id="disk-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
<div id="disk-list">{{T "storage_init.betoltes"}}</div>
</div>
<div class="settings-card" id="cfg-card" style="display:none">
<h3>{{T "storage_init.2_konfiguracio"}}</h3>
<form id="init-form" onsubmit="return submitInit(event)">
<div class="form-group">
<label>{{T "storage_init.kivalasztott_eszkoz"}}</label>
<span class="settings-value mono" id="sel-device">—</span>
</div>
<div class="form-group">
<label for="mount-name">{{T "storage_init.csatlakoztatasi_nev"}} <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">{{T "storage_init.a_meghajto_a_mnt_nev"}}</span>
</div>
<div class="form-group">
<label for="fstype">{{T "storage_init.fajlrendszer"}}</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">{{T "storage_init.megnevezes"}}</label>
<input type="text" id="storage-label" class="form-control" placeholder="{{T "storage_init.kulso_hdd_1tb"}}" maxlength="50">
</div>
<label class="toggle" style="margin-bottom:1.25rem">
<input type="checkbox" id="set-default" checked>
<span class="toggle-label">{{T "storage_init.beallitas_alapertelmezett_adattarolokent"}}</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> {{T "storage_init.a_kivalasztott_meghajto_adatot_tartalmaz"}}
</div>
<div class="form-actions" style="gap:.75rem">
<button type="submit" class="btn btn-danger-outline" id="init-btn">{{T "storage_init.inicializalas_inditasa"}}</button>
<a href="/settings" class="btn btn-outline">{{T "storage_init.megsem"}}</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">{{T "storage_init.adatot_tartalmaz_a_formazas_torli"}}</span>'
: '<span class="badge badge-ok">{{T "storage_init.ures_formazhato"}}</span>';
}
function fmtSize(b){
if(!b) return '';
var g=b/1073741824;
return g>=1 ? fhNum(g, 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 excludes OS/claimed disks (fail-safe) and the proxy removes every drive behind a
// registered storage path (R-542), 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||'{{T "storage_init.hiba"}}'); }
var cands = (j.data&&j.data.initialize)||[];
if(cands.length===0){ document.getElementById('disk-list').innerHTML='<p class="form-hint">{{T "storage_init.nincs_elerheto_meghajto_az_inicializalas"}}</p>'; return; }
var html='<div class="drive-list">';
var n=0;
function card(dev, title, sub, db){
var i=n++;
return '<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-db="'+(db?'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({data_bearing:db})+'</div></div></label>';
}
cands.forEach(function(d){
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('{{T "storage_init.jelenlegi"}} '+esc(d.fstype));
// `16` §6: a disk whose other part is already a drive offers only its free parts, never the whole disk.
if(!d.whole_disk_in_use) html+=card(d.device, title, parts.join(' · '), d.data_bearing);
(d.partitions||[]).forEach(function(p){
// only a part the agent says can be formatted (nothing else on the disk is mounted — its root wrapper refuses otherwise)
if((p.state!=='empty' && p.state!=='filesystem') || !p.formattable) return;
var ps=[esc(p.device)]; if(p.size_bytes) ps.push(fmtSize(p.size_bytes)); if(p.fstype) ps.push('{{T "storage_init.jelenlegi"}} '+esc(p.fstype));
html+=card(p.device, title+' — '+'{{T "storage_wizard.resz_n"}}'.replace('{n}', p.number||'?'), ps.join(' · '), p.state==='filesystem');
});
});
html+='</div>';
document.getElementById('disk-list').innerHTML=html;
}catch(e){ var el=document.getElementById('disk-error'); el.style.display='block'; el.textContent='{{T "storage_init.meghajtok_betoltese_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=['{{T "storage_init.formazas"}}','{{T "storage_init.csatolas"}}','{{T "storage_init.regisztralas"}}'];
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?'{{T "storage_init.kesz"}}':(i===cur?'{{T "storage_init.folyamatban"}}':'{{T "storage_init.var"}}');
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">{{T "storage_init.az_allapot_lekerdezese_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">{{T "storage_init.operatori_alairas_szukseges_ez_a"}}'
+(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">{{T "storage_init.hiba_2"}} '+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)||'{{T "storage_init.mar_folyamatban_van_egy_inicializalas"}}')+'</div>'; btn.disabled=false; return false; }
if(!res.j.ok){ out.innerHTML='<div class="alert alert-error">{{T "storage_init.hiba_2"}} '+esc(res.j.error||'')+'</div>'; btn.disabled=false; return false; }
await pollInitStatus(out, btn);
}catch(e){ out.innerHTML='<div class="alert alert-error">{{T "storage_init.hiba_2"}} '+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">{{T "storage_init.a_meghajto_adatot_tartalmaz_a"}}</div>'
+'<div class="confirm-input"><label>{{T "storage_init.megerositeshez_irja_be_a_csatlakoztatasi"}} <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 "storage_init.torles_es_inicializalas_megerositese"}}</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 "storage_init.torles_es_inicializalas_inditasa"}}</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">{{T "storage_init.hiba_2"}} '+esc(res2.j.error||'')+'</div>'; return; }
out.innerHTML=stepper('formatting');
await pollInitStatus(out, btn);
}catch(e){ cr.innerHTML='<div class="alert alert-error">{{T "storage_init.hiba_2"}} '+esc(e.message)+'</div>'; }
};
}
function finishInit(where, out){
out.innerHTML='<div class="alert alert-success">{{T "storage_init.a_meghajto_sikeresen_inicializalva_es"}} <strong class="mono">'+esc(where)+'</strong>{{T "storage_init.vissza_a_tarhelyhez"}}</div>';
}
loadDisks();
</script>
{{template "layout_end" .}}
{{end}}