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
gates / gates (push) Successful in 1m37s
This commit is contained in:
@@ -64,6 +64,23 @@
|
||||
titkosított adat van. Ez a másolat független a helyi másodpéldánytól és a teljes rendszermentéstől." New parity case
|
||||
`backups_remote_pinned`; the `backups_remote_*`, `escrow_first`, `escrow_receremony` and `settings_security_*`
|
||||
fixtures regenerated on purpose (only the lines above changed).
|
||||
- **W13 — the new-drive pages never refuse in silence.** The mount name is pre-filled with the next free `hdd_N`
|
||||
(`hdd_1` when none; every registered path counts as taken, a decommissioned one too — `nextFreeMountName`), the hint
|
||||
adds „Hagyd így, ha nem tudod." (EN „Leave it as it is if you are not sure."), and the form is `novalidate`: an empty
|
||||
or wrong name answers with a visible sentence, „Írj be egy csatlakoztatási nevet. Csak betű, szám, _ és - lehet
|
||||
benne, legfeljebb 40 karakter. Ha nem tudod, írd be: hdd_2" (EN „Type a mount name. Only letters, digits, _ and - are
|
||||
allowed, at most 40 characters. If you are not sure, type: hdd_2"), never only a browser tooltip. The same on the
|
||||
attach page (same defect). `TestW13_StorageInitSuggestsTheNextFreeNameAndNeverFailsSilently`; `storage_init` and
|
||||
`storage_attach` fixtures regenerated on purpose.
|
||||
- **W14 — the storage page's drive cards** keep the type, device, path and durable id („local-lvm lvmthin · pve/data",
|
||||
„10.77.0.1:felhom-offsite#…") behind „Részletek" / „Details"; the name, purpose line, badges and fill bar stay. The
|
||||
`storage_full` / `storage_empty` fixtures regenerated on purpose.
|
||||
- **W29 — a plain decommission no longer shows the move sentence with empty blanks** („Adatok átköltöztetve ide: .
|
||||
()"). With nothing moved the card reads „Leszerelve. Nem költöztettünk át adatot róla; ami rajta volt, a meghajtón
|
||||
maradt." (EN „Decommissioned. No data was moved off it; whatever was on it stays on the drive."). NOT the brief's
|
||||
„Leszerelve, adat nem volt rajta": a decommission-anyway stops the apps on the drive and unmounts it without moving or
|
||||
formatting, so the data may well still be on it. New parity case `storage_decommissioned_plain`;
|
||||
`TestW29_PlainDecommissionNamesNoMove`.
|
||||
|
||||
## v0.337.0 (2026-10-21) — kept second-drive copies (D25: R-1175, R-1181, R-1179); a `.fab` import keeps the family gate (R-1191); a stale restore temp no longer blocks the retry (R-1188 c); the overcommit sentence made true (R-1192); escrow recorded before the floor (R-1176); a finished off-site abandonment closes on the hub's word (R-1182); storage labels follow the language (W26)
|
||||
|
||||
|
||||
@@ -1409,6 +1409,7 @@
|
||||
"err.stacks.field_required": "the %q (%s) field has to be filled in",
|
||||
"err.stacks.needs_drive": "This app needs a storage drive. You can add one on the Storage → Drives page.",
|
||||
"storage_wizard.resz_n": "part {n}",
|
||||
"storage_wizard.name_invalid": "Type a mount name. Only letters, digits, _ and - are allowed, at most 40 characters. If you are not sure, type: {name}",
|
||||
"err.web.meghajto_hasznalatban_formazas": "This disk holds a drive that is in use, so the whole disk cannot be formatted. You can format only its free part.",
|
||||
"storage.belso_tarhely_magyarazat": "It is on the machine's own disk, but it is not part of the full system backup. A different disk or the off-site backup protects against a disk failure.",
|
||||
"backup.target.offer_unproven": "You can choose this drive as the place for the system backup. Felhom cannot check that this drive is really a different disk (inside a virtual machine and behind some USB enclosures this is not visible), so we cannot promise that you can restore the system after a disk failure.",
|
||||
@@ -2277,6 +2278,7 @@
|
||||
"storage.a_z": "Drive",
|
||||
"storage.adatok_athelyezese": "Moving data",
|
||||
"storage.adatok_atkoltoztetve_ide": "Data moved to: <strong>{{.MigratedToLabel}}</strong> ({{.MigratedTo}})",
|
||||
"storage.leszerelve_nem_koltoztettunk": "Decommissioned. No data was moved off it; whatever was on it stays on the drive.",
|
||||
"storage.adatok_masolasa": "Copying data",
|
||||
"storage.adatot_tartalmaz": "Holds data",
|
||||
"storage.adattarolok": "Data storage",
|
||||
@@ -2393,6 +2395,7 @@
|
||||
"storage.regisztracio_sikertelen": "Registration failed:",
|
||||
"storage.regisztralas": "Register",
|
||||
"storage.regisztralva": "Registered",
|
||||
"storage.reszletek": "Details",
|
||||
"storage.rendszer_vedett": "System — protected",
|
||||
"storage.rendszermeghajtok": "System drives",
|
||||
"storage.rendszermeghajton": "On the system drive",
|
||||
@@ -2412,7 +2415,7 @@
|
||||
"storage.visszacsatlakoztatja_a_z": "Reconnect the",
|
||||
"storage_attach.1_meghajto_kivalasztasa": "1. Choose a drive",
|
||||
"storage_attach.2_konfiguracio": "2. Configuration",
|
||||
"storage_attach.a_meghajto_a_mnt_nev": "The drive goes to /mnt/<name>.",
|
||||
"storage_attach.a_meghajto_a_mnt_nev": "The drive goes to /mnt/<name>. Leave it as it is if you are not sure.",
|
||||
"storage_attach.a_meghajto_sikeresen": "The drive was",
|
||||
"storage_attach.beallitas_alapertelmezett_adattarolokent": "Use as the default data storage for new installs",
|
||||
"storage_attach.betoltes": "Loading…",
|
||||
@@ -2439,7 +2442,7 @@
|
||||
"storage_init.1_meghajto_kivalasztasa": "1. Choose a drive",
|
||||
"storage_init.2_konfiguracio": "2. Configuration",
|
||||
"storage_init.a_kivalasztott_meghajto_adatot_tartalmaz": "The chosen drive <strong>holds data</strong>. Initialising (formatting) deletes all data\n on it — you have to confirm to continue.",
|
||||
"storage_init.a_meghajto_a_mnt_nev": "The drive goes to /mnt/<name>.",
|
||||
"storage_init.a_meghajto_a_mnt_nev": "The drive goes to /mnt/<name>. Leave it as it is if you are not sure.",
|
||||
"storage_init.a_meghajto_adatot_tartalmaz_a": "The drive holds data. Formatting <strong>deletes for good</strong> all data on it.",
|
||||
"storage_init.a_meghajto_sikeresen_inicializalva_es": "The drive is initialised and registered:",
|
||||
"storage_init.adatot_tartalmaz_a_formazas_torli": "Holds data — formatting deletes it",
|
||||
|
||||
@@ -1402,6 +1402,7 @@
|
||||
"err.stacks.field_required": "a(z) %q (%s) mező kitöltése kötelező",
|
||||
"err.stacks.needs_drive": "Ehhez az alkalmazáshoz egy adattároló kell. A Meghajtók oldalon adhatsz hozzá egyet.",
|
||||
"storage_wizard.resz_n": "{n}. rész",
|
||||
"storage_wizard.name_invalid": "Írj be egy csatlakoztatási nevet. Csak betű, szám, _ és - lehet benne, legfeljebb 40 karakter. Ha nem tudod, írd be: {name}",
|
||||
"err.web.meghajto_hasznalatban_formazas": "Ezen a lemezen egy használatban lévő meghajtó van, ezért a teljes lemez nem formázható. Csak a szabad részét formázhatod.",
|
||||
"storage.belso_tarhely_magyarazat": "A gép saját lemezén van, de nem része a teljes rendszermentésnek. Egy lemezhiba ellen egy másik lemez vagy a távoli mentés véd.",
|
||||
"backup.target.offer_unproven": "Ezt a meghajtót kijelölheted a rendszermentés helyéül. A Felhom nem tudja ellenőrizni, hogy ez a meghajtó valóban másik lemez-e (virtuális gépben és egyes USB-házakban ez nem látszik), ezért nem ígérhetjük, hogy egy lemezhiba után is vissza tudod állítani a rendszert.",
|
||||
@@ -2265,6 +2266,7 @@
|
||||
"storage.a_z": "A(z)",
|
||||
"storage.adatok_athelyezese": "Adatok áthelyezése",
|
||||
"storage.adatok_atkoltoztetve_ide": "Adatok átköltöztetve ide: <strong>{{.MigratedToLabel}}</strong> ({{.MigratedTo}})",
|
||||
"storage.leszerelve_nem_koltoztettunk": "Leszerelve. Nem költöztettünk át adatot róla; ami rajta volt, a meghajtón maradt.",
|
||||
"storage.adatok_masolasa": "Adatok másolása",
|
||||
"storage.adatot_tartalmaz": "Adatot tartalmaz",
|
||||
"storage.adattarolok": "Adattárolók",
|
||||
@@ -2381,6 +2383,7 @@
|
||||
"storage.regisztracio_sikertelen": "Regisztráció sikertelen:",
|
||||
"storage.regisztralas": "Regisztrálás",
|
||||
"storage.regisztralva": "Regisztrálva",
|
||||
"storage.reszletek": "Részletek",
|
||||
"storage.rendszer_vedett": "Rendszer — védett",
|
||||
"storage.rendszermeghajtok": "Rendszermeghajtók",
|
||||
"storage.rendszermeghajton": "Rendszermeghajtón",
|
||||
@@ -2400,7 +2403,7 @@
|
||||
"storage.visszacsatlakoztatja_a_z": "Visszacsatlakoztatod a(z)",
|
||||
"storage_attach.1_meghajto_kivalasztasa": "1. Meghajtó kiválasztása",
|
||||
"storage_attach.2_konfiguracio": "2. Konfiguráció",
|
||||
"storage_attach.a_meghajto_a_mnt_nev": "A meghajtó a /mnt/<név> útvonalra kerül.",
|
||||
"storage_attach.a_meghajto_a_mnt_nev": "A meghajtó a /mnt/<név> útvonalra kerül. Hagyd így, ha nem tudod.",
|
||||
"storage_attach.a_meghajto_sikeresen": "A meghajtó sikeresen",
|
||||
"storage_attach.beallitas_alapertelmezett_adattarolokent": "Beállítás alapértelmezett adattárolóként új telepítéseknél",
|
||||
"storage_attach.betoltes": "Betöltés…",
|
||||
@@ -2427,7 +2430,7 @@
|
||||
"storage_init.1_meghajto_kivalasztasa": "1. Meghajtó kiválasztása",
|
||||
"storage_init.2_konfiguracio": "2. Konfiguráció",
|
||||
"storage_init.a_kivalasztott_meghajto_adatot_tartalmaz": "A kiválasztott meghajtó <strong>adatot tartalmaz</strong>. Az inicializálás (formázás) törli a rajta lévő\n összes adatot — a folytatáshoz meg kell erősítened.",
|
||||
"storage_init.a_meghajto_a_mnt_nev": "A meghajtó a /mnt/<név> útvonalra kerül.",
|
||||
"storage_init.a_meghajto_a_mnt_nev": "A meghajtó a /mnt/<név> útvonalra kerül. Hagyd így, ha nem tudod.",
|
||||
"storage_init.a_meghajto_adatot_tartalmaz_a": "A meghajtó adatot tartalmaz. A formázás <strong>véglegesen törli</strong> a rajta lévő összes adatot.",
|
||||
"storage_init.a_meghajto_sikeresen_inicializalva_es": "A meghajtó sikeresen inicializálva és regisztrálva:",
|
||||
"storage_init.adatot_tartalmaz_a_formazas_torli": "Adatot tartalmaz — a formázás törli",
|
||||
|
||||
@@ -40,6 +40,12 @@ func i18nCasesC() []i18nCase {
|
||||
{"storage_empty", "storage", func() map[string]interface{} {
|
||||
return i18nLayoutData("storage", "Tárhely")
|
||||
}},
|
||||
// W29: a plain decommission (nothing moved) says so, never the move sentence with empty blanks.
|
||||
{"storage_decommissioned_plain", "storage", func() map[string]interface{} {
|
||||
d := i18nLayoutData("storage", "Tárhely")
|
||||
d["StoragePaths"] = []m{{"Path": "/mnt/hdd_1", "Label": "hdd_1", "Decommissioned": true, "DecommissionedAt": "2026-10-20 18:00"}}
|
||||
return d
|
||||
}},
|
||||
// `09` §3 decision 36 — the kept-data list, one dated and one leftover row, and the empty page.
|
||||
{"kept_data_full", "kept_data", func() map[string]interface{} {
|
||||
d := i18nLayoutData("kept-data", "Megőrzött adatok")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,6 +55,22 @@ func mountWhere(name string) (string, error) {
|
||||
return "/mnt/" + name, nil
|
||||
}
|
||||
|
||||
// nextFreeMountName is the first hdd_N (N from 1) whose /mnt/hdd_N is not a registered storage path — a
|
||||
// decommissioned one counts as taken, so a new drive never reuses an old drive's name (W13). Pinned by
|
||||
// TestW13_StorageInitSuggestsTheNextFreeNameAndNeverFailsSilently.
|
||||
func nextFreeMountName(registered []settings.StoragePath) string {
|
||||
taken := map[string]bool{}
|
||||
for _, sp := range registered {
|
||||
taken[path.Clean(sp.Path)] = true
|
||||
}
|
||||
for n := 1; ; n++ {
|
||||
name := "hdd_" + strconv.Itoa(n)
|
||||
if !taken["/mnt/"+name] {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fsUUIDForDevice re-lists the agent's disks and returns the fs UUID of the storage backed by `device`
|
||||
// (the only way the de-privileged controller learns the UUID it must pass to assign). "" if not found.
|
||||
func fsUUIDForDevice(disks agentapi.DisksResponse, device string) string {
|
||||
@@ -373,6 +390,12 @@ func (s *Server) storageWizardPageHandler(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
data := s.baseData(tmpl, title)
|
||||
data["TitleKey"] = titleKey // i18n: the Hungarian title above is what hu renders
|
||||
// W13 (rehearsal 2026-10-20): the mount-name box was empty and a household did not know what to type.
|
||||
var registered []settings.StoragePath
|
||||
if s.settings != nil {
|
||||
registered = s.settings.GetStoragePaths()
|
||||
}
|
||||
data["SuggestedMountName"] = nextFreeMountName(registered)
|
||||
// executeTemplate, NOT render(): the wizard POSTs /api/storage/* with the csrf-meta token,
|
||||
// and render() leaves {{.CSRFToken}} empty. Latent until the claim arc — on an unclaimed
|
||||
// (auth-off) box CsrfProtect is skipped entirely, so the wizard had never run against a
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
|
||||
<div class="settings-card" id="cfg-card" style="display:none">
|
||||
<h3>2. Konfiguráció</h3>
|
||||
<form id="attach-form" onsubmit="return submitAttach(event)">
|
||||
<form id="attach-form" novalidate onsubmit="return submitAttach(event)">
|
||||
<div class="form-group">
|
||||
<label>Kiválasztott eszköz</label>
|
||||
<span class="settings-value mono" id="sel-device">—</span>
|
||||
@@ -199,10 +199,10 @@
|
||||
<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">
|
||||
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1" value="hdd_1"
|
||||
pattern="[a-zA-Z0-9_-]+" maxlength="40" required style="max-width:180px">
|
||||
</div>
|
||||
<span class="form-hint">A meghajtó a /mnt/<név> útvonalra kerül.</span>
|
||||
<span class="form-hint">A meghajtó a /mnt/<név> útvonalra kerül. Hagyd így, ha nem tudod.</span>
|
||||
</div>
|
||||
<p class="form-hint" id="mounted-note" style="display:none">
|
||||
Ez a meghajtó már csatlakoztatva van, csak a gép nem tartja nyilván. A „Csatolás" a
|
||||
@@ -295,9 +295,20 @@ function pickDisk(radio){
|
||||
document.getElementById('cfg-card').scrollIntoView({behavior:'smooth'});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function mountNameProblem(){
|
||||
if(selMounted){ return ''; }
|
||||
var v=(document.getElementById('mount-name').value||'').trim();
|
||||
if(/^[a-zA-Z0-9_-]{1,40}$/.test(v)){ return ''; }
|
||||
return 'Írj be egy csatlakoztatási nevet. Csak betű, szám, _ és - lehet benne, legfeljebb 40 karakter. Ha nem tudod, írd be: {name}'.replace('{name}', 'hdd_1');
|
||||
}
|
||||
|
||||
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">Csatlakoztatás folyamatban…</p>';
|
||||
try{
|
||||
|
||||
@@ -309,7 +320,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)});
|
||||
|
||||
+1122
File diff suppressed because it is too large
Load Diff
@@ -421,11 +421,14 @@ window.__registeredPaths=[];
|
||||
var badges = roleTag(d.role)+appBackingTag(d)+classTag(d)+dataTag(d);
|
||||
if(withActions) badges += regTag(d, registered);
|
||||
var purpose = purposeDesc(d);
|
||||
|
||||
|
||||
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>Részletek</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>';
|
||||
@@ -438,8 +441,8 @@ window.__registeredPaths=[];
|
||||
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="Tartós azonosító">'+esc(d.durable_id)+'</span>':'')
|
||||
+'</div>'
|
||||
+(d.durable_id?'<details class="drive-details"><summary>Részletek</summary><span class="drive-sub mono" title="Tartós azonosító">'+esc(d.durable_id)+'</span></details>':'')
|
||||
+actions(d,registered,hasSafeDisconnect);
|
||||
slot.innerHTML=extra;
|
||||
return true;
|
||||
|
||||
@@ -788,11 +788,14 @@ window.__registeredPaths=["\/mnt\/usb1","\/mnt\/old","\/mnt\/hdd1","\/mnt\/hdd2"
|
||||
var badges = roleTag(d.role)+appBackingTag(d)+classTag(d)+dataTag(d);
|
||||
if(withActions) badges += regTag(d, registered);
|
||||
var purpose = purposeDesc(d);
|
||||
|
||||
|
||||
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>Részletek</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>';
|
||||
@@ -805,8 +808,8 @@ window.__registeredPaths=["\/mnt\/usb1","\/mnt\/old","\/mnt\/hdd1","\/mnt\/hdd2"
|
||||
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="Tartós azonosító">'+esc(d.durable_id)+'</span>':'')
|
||||
+'</div>'
|
||||
+(d.durable_id?'<details class="drive-details"><summary>Részletek</summary><span class="drive-sub mono" title="Tartós azonosító">'+esc(d.durable_id)+'</span></details>':'')
|
||||
+actions(d,registered,hasSafeDisconnect);
|
||||
slot.innerHTML=extra;
|
||||
return true;
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
|
||||
<div class="settings-card" id="cfg-card" style="display:none">
|
||||
<h3>2. Konfiguráció</h3>
|
||||
<form id="init-form" onsubmit="return submitInit(event)">
|
||||
<form id="init-form" novalidate onsubmit="return submitInit(event)">
|
||||
<div class="form-group">
|
||||
<label>Kiválasztott eszköz</label>
|
||||
<span class="settings-value mono" id="sel-device">—</span>
|
||||
@@ -199,10 +199,10 @@
|
||||
<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">
|
||||
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1" value="hdd_1"
|
||||
pattern="[a-zA-Z0-9_-]+" maxlength="40" required style="max-width:180px">
|
||||
</div>
|
||||
<span class="form-hint">A meghajtó a /mnt/<név> útvonalra kerül.</span>
|
||||
<span class="form-hint">A meghajtó a /mnt/<név> útvonalra kerül. Hagyd így, ha nem tudod.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fstype">Fájlrendszer</label>
|
||||
@@ -298,7 +298,7 @@ function pickDisk(radio){
|
||||
|
||||
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()};
|
||||
@@ -341,9 +341,19 @@ async function pollInitStatus(out, btn){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function mountNameProblem(){
|
||||
var v=(document.getElementById('mount-name').value||'').trim();
|
||||
if(/^[a-zA-Z0-9_-]{1,40}$/.test(v)){ return ''; }
|
||||
return 'Írj be egy csatlakoztatási nevet. Csak betű, szám, _ és - lehet benne, legfeljebb 40 karakter. Ha nem tudod, írd be: {name}'.replace('{name}', 'hdd_1');
|
||||
}
|
||||
|
||||
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, "");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// W13 (rehearsal 2026-10-20): the init page's mount-name box was empty („a household does not know what to type") and
|
||||
// a press with an empty box did nothing visible — only the browser's tooltip, from `required`. The box now holds the
|
||||
// next free hdd_N, the hint says „Hagyd így, ha nem tudod", the form is novalidate and the press answers a bad name
|
||||
// with a visible sentence (the server checks the name again: mountWhere).
|
||||
func TestW13_StorageInitSuggestsTheNextFreeNameAndNeverFailsSilently(t *testing.T) {
|
||||
if got := nextFreeMountName(nil); got != "hdd_1" {
|
||||
t.Fatalf("no drive: %q", got)
|
||||
}
|
||||
reg := []settings.StoragePath{{Path: "/mnt/hdd_1"}, {Path: "/mnt/hdd_3/"}, {Path: "/mnt/felhom-drives/nas"}, {Path: "/mnt/hdd_2", Decommissioned: true}}
|
||||
if got := nextFreeMountName(reg); got != "hdd_4" {
|
||||
t.Fatalf("hdd_1, hdd_2 (decommissioned) and hdd_3 taken: %q", got)
|
||||
}
|
||||
if got := nextFreeMountName(reg[:2]); got != "hdd_2" {
|
||||
t.Fatalf("hdd_1 and hdd_3 taken: %q", got)
|
||||
}
|
||||
|
||||
s := testServer(t)
|
||||
s.loadTemplates()
|
||||
if err := s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/hdd_1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, page := range []string{"storage_init", "storage_attach"} {
|
||||
rr := httptest.NewRecorder()
|
||||
s.storageWizardPageHandler(rr, httptest.NewRequest("GET", "/storage/init", nil), page)
|
||||
body := rr.Body.String()
|
||||
if !strings.Contains(body, `id="mount-name" class="form-control" placeholder="hdd_1" value="hdd_2"`) {
|
||||
t.Errorf("%s: the mount name is not pre-filled with the next free name", page)
|
||||
}
|
||||
if !strings.Contains(body, "Hagyd így, ha nem tudod.") {
|
||||
t.Errorf("%s: the hint does not say „Hagyd így, ha nem tudod.”", page)
|
||||
}
|
||||
// THE CONSEQUENCE: no browser-only refusal — the form never lets the browser swallow the press, and the
|
||||
// press has a visible sentence for a bad name.
|
||||
if !strings.Contains(body, `-form" novalidate onsubmit=`) {
|
||||
t.Errorf("%s: the form is not novalidate — an empty name is refused by a tooltip only", page)
|
||||
}
|
||||
if !strings.Contains(body, "var bad=mountNameProblem();") || !strings.Contains(body, `id="mount-name-error"`) ||
|
||||
!strings.Contains(body, "Írj be egy csatlakoztatási nevet.") {
|
||||
t.Errorf("%s: a bad name has no visible answer", page)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// W29 (rehearsal 2026-10-20): after a plain decommission (nothing moved) the storage card read „Kiváltva — Adatok
|
||||
// átköltöztetve ide: . () Időpont: …" — the move sentence with empty blanks. With no move it says the drive was
|
||||
// decommissioned and that nothing was moved off it; a real move keeps its sentence.
|
||||
func TestW29_PlainDecommissionNamesNoMove(t *testing.T) {
|
||||
s := i18nTestServer(t)
|
||||
want := map[string][2]string{
|
||||
"hu": {"Leszerelve. Nem költöztettünk át adatot róla", "Adatok átköltöztetve ide"},
|
||||
"en": {"Decommissioned. No data was moved off it", "Data moved to"},
|
||||
}
|
||||
for _, c := range i18nCases() {
|
||||
switch c.name {
|
||||
case "storage_decommissioned_plain":
|
||||
for lang, w := range want {
|
||||
page := renderI18nCase(t, s, lang, c)
|
||||
if !strings.Contains(page, w[0]) || strings.Contains(page, w[1]) {
|
||||
t.Errorf("%s: a plain decommission must say %q and never %q", lang, w[0], w[1])
|
||||
}
|
||||
}
|
||||
case "storage_full": // a real move keeps its sentence
|
||||
for lang, w := range want {
|
||||
page := renderI18nCase(t, s, lang, c)
|
||||
if !strings.Contains(page, w[1]) || strings.Contains(page, w[0]) {
|
||||
t.Errorf("%s: a decommission with a move must say %q", lang, w[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user