W13 new-drive pages pre-fill the next free hdd_N and answer a bad name visibly; W14 drive operator detail behind Részletek; W29 plain decommission names no move; unreleased
gates / gates (push) Successful in 1m37s

This commit is contained in:
2026-10-21 22:51:08 +02:00
parent 8c1e290f63
commit 28b05d6d68
16 changed files with 1349 additions and 32 deletions
@@ -55,7 +55,9 @@
{{else if .Decommissioned}}
<div class="storage-path-details">
<div class="storage-disconnected-info">
<span class="form-hint">{{T "storage.adatok_atkoltoztetve_ide"}}</span>
{{- /* W29 (rehearsal 2026-10-20): a plain decommission moves nothing; the move sentence rendered
with empty blanks („Adatok átköltöztetve ide: . ()"). It names a move only when there was one. */}}
{{if .MigratedTo}}<span class="form-hint">{{T "storage.adatok_atkoltoztetve_ide"}}</span>{{else}}<span class="form-hint">{{T "storage.leszerelve_nem_koltoztettunk"}}</span>{{end}}
{{if .DecommissionedAt}}<span class="form-hint">{{T "storage.idopont"}}</span>{{end}}
</div>
</div>
@@ -342,11 +344,14 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
var badges = roleTag(d.role)+appBackingTag(d)+classTag(d)+dataTag(d);
if(withActions) badges += regTag(d, registered);
var purpose = purposeDesc(d);
// W14 (rehearsal 2026-10-20): the type, device, path and durable id („local-lvm lvmthin · pve/data",
// „10.77.0.1:felhom-offsite#…") are operator detail; they wait behind „Részletek".
return '<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-card-top"><div class="drive-id"><span class="drive-name">'+esc(d.name)+'</span></div>'
+'<div class="drive-badges">'+badges+'</div></div>'
+(purpose?'<div class="drive-purpose">'+esc(purpose)+'</div>':'')
+(d.durable_id?'<div class="drive-sub mono">'+esc(d.durable_id)+'</div>':'')
+'<details class="drive-details"><summary>{{T "storage.reszletek"}}</summary><div class="drive-sub">'+sub+'</div>'
+(d.durable_id?'<div class="drive-sub mono">'+esc(d.durable_id)+'</div>':'')+'</details>'
+capBar(d)
+(withActions?actions(d,registered):'')
+'</div>';
@@ -359,8 +364,8 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
var extra='<div class="metarows">'
+'<span class="metarow">'+roleTag(d.role)+'</span>'
+(d.class?'<span class="metarow">'+classTag(d)+'</span>':'')
+(d.durable_id?'<span class="metarow mono" title="{{T "storage.tartos_azonosito"}}">'+esc(d.durable_id)+'</span>':'')
+'</div>'
+(d.durable_id?'<details class="drive-details"><summary>{{T "storage.reszletek"}}</summary><span class="drive-sub mono" title="{{T "storage.tartos_azonosito"}}">'+esc(d.durable_id)+'</span></details>':'')
+actions(d,registered,hasSafeDisconnect);
slot.innerHTML=extra;
return true;
@@ -17,7 +17,7 @@
<div class="settings-card" id="cfg-card" style="display:none">
<h3>{{T "storage_attach.2_konfiguracio"}}</h3>
<form id="attach-form" onsubmit="return submitAttach(event)">
<form id="attach-form" novalidate onsubmit="return submitAttach(event)">
<div class="form-group">
<label>{{T "storage_attach.kivalasztott_eszkoz"}}</label>
<span class="settings-value mono" id="sel-device">—</span>
@@ -28,8 +28,8 @@
<label for="mount-name">{{T "storage_attach.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">
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1" value="{{if .SuggestedMountName}}{{.SuggestedMountName}}{{else}}hdd_1{{end}}"
pattern="[a-zA-Z0-9_-]+" maxlength="40" required style="max-width:180px">
</div>
<span class="form-hint">{{T "storage_attach.a_meghajto_a_mnt_nev"}}</span>
</div>
@@ -123,9 +123,20 @@ function pickDisk(radio){
document.getElementById('cfg-card').scrollIntoView({behavior:'smooth'});
}
// W13: the form is novalidate — a browser tooltip was the only answer to an empty or wrong name, and the button
// seemed to do nothing. An already-mounted store has no name to check.
function mountNameProblem(){
if(selMounted){ return ''; }
var v=(document.getElementById('mount-name').value||'').trim();
if(/^[a-zA-Z0-9_-]{1,40}$/.test(v)){ return ''; }
return '{{T "storage_wizard.name_invalid"}}'.replace('{name}', '{{if .SuggestedMountName}}{{.SuggestedMountName}}{{else}}hdd_1{{end}}');
}
async function submitAttach(ev){
ev.preventDefault();
var btn=document.getElementById('attach-btn'); var out=document.getElementById('attach-result');
var bad=mountNameProblem();
if(bad){ out.innerHTML='<div class="alert alert-error" id="mount-name-error">'+esc(bad)+'</div>'; document.getElementById('mount-name').focus(); return false; }
btn.disabled=true; out.innerHTML='<p class="form-hint">{{T "storage_attach.csatlakoztatas_folyamatban"}}</p>';
try{
// R-280: an already-mounted store is REGISTERED in place. Sending it to /api/storage/attach would
@@ -137,7 +148,7 @@ async function submitAttach(ev){
set_default:document.getElementById('set-default').checked};
}else{
url='/api/storage/attach';
body={device:selDevice, fstype:selFSType, mount_name:document.getElementById('mount-name').value,
body={device:selDevice, fstype:selFSType, mount_name:document.getElementById('mount-name').value.trim(),
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)});
@@ -17,7 +17,7 @@
<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)">
<form id="init-form" novalidate onsubmit="return submitInit(event)">
<div class="form-group">
<label>{{T "storage_init.kivalasztott_eszkoz"}}</label>
<span class="settings-value mono" id="sel-device">—</span>
@@ -26,8 +26,8 @@
<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">
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1" value="{{if .SuggestedMountName}}{{.SuggestedMountName}}{{else}}hdd_1{{end}}"
pattern="[a-zA-Z0-9_-]+" maxlength="40" required style="max-width:180px">
</div>
<span class="form-hint">{{T "storage_init.a_meghajto_a_mnt_nev"}}</span>
</div>
@@ -124,7 +124,7 @@ function pickDisk(radio){
// 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,
mount_name:document.getElementById('mount-name').value.trim(), 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()};
@@ -167,9 +167,19 @@ async function pollInitStatus(out, btn){
}
}
// W13: the form is novalidate — a browser tooltip was the only answer to an empty or wrong name, and the button
// seemed to do nothing. mountNameProblem answers with a visible sentence instead (the server checks again).
function mountNameProblem(){
var v=(document.getElementById('mount-name').value||'').trim();
if(/^[a-zA-Z0-9_-]{1,40}$/.test(v)){ return ''; }
return '{{T "storage_wizard.name_invalid"}}'.replace('{name}', '{{if .SuggestedMountName}}{{.SuggestedMountName}}{{else}}hdd_1{{end}}');
}
async function submitInit(ev){
ev.preventDefault();
var btn=document.getElementById('init-btn'); var out=document.getElementById('init-result');
var bad=mountNameProblem();
if(bad){ out.innerHTML='<div class="alert alert-error" id="mount-name-error">'+esc(bad)+'</div>'; document.getElementById('mount-name').focus(); return false; }
btn.disabled=true; out.innerHTML=stepper('formatting');
try{
var res=await postInit(false, "");
@@ -3641,6 +3641,9 @@ a.stat-card:hover {
font-size: .78rem; color: var(--text-3);
font-family: var(--font-data); word-break: break-all;
}
/* W14: a drive's operator detail (type, device, path, durable id) waits behind „Részletek". */
.drive-details summary { cursor: pointer; font-size: .78rem; color: var(--text-3); }
.drive-details[open] summary { margin-bottom: .2rem; }
.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 .meter-track { height: 3px; }