diff --git a/CHANGELOG.md b/CHANGELOG.md index aab2e78..5fa64f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index 68cf533..b1ac7bc 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -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: {{.MigratedToLabel}} ({{.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 holds data. 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 deletes for good 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", diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index ee8b3b7..edf8c44 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -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: {{.MigratedToLabel}} ({{.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ó adatot tartalmaz. 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 véglegesen törli 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", diff --git a/controller/internal/web/i18n_cases_c_test.go b/controller/internal/web/i18n_cases_c_test.go index 4393580..2e0bc2b 100644 --- a/controller/internal/web/i18n_cases_c_test.go +++ b/controller/internal/web/i18n_cases_c_test.go @@ -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") diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index 0c34619..0eabfb5 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -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 diff --git a/controller/internal/web/templates/storage.html b/controller/internal/web/templates/storage.html index f24e54f..785b4c5 100644 --- a/controller/internal/web/templates/storage.html +++ b/controller/internal/web/templates/storage.html @@ -55,7 +55,9 @@ {{else if .Decommissioned}}