diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa64f8..163e257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,29 @@ „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`. +- **W21 — switching sharing on with no password says it waits for one.** The service is never deployed until the + household sets the sharing password (reconcile: „deploy skipped — household SMB password not set yet"), yet the + answer said „…előkészítése folyamatban…" and the page stayed „áll". Now the switch-on answers „Beállítás mentve. A + megosztás akkor indul el, amikor beállítod a megosztási jelszót." (EN „Setting saved. Sharing starts when you set the + sharing password.", key `flash.sharing.saved_needs_password`) and starts no bring-up job; the page shows „Állíts be + egy megosztási jelszót — enélkül a megosztás nem indul el." + „Jelszó beállítása" (EN „Set a sharing password. + Without it, sharing does not start." + „Set the password") and focuses the password field. + `TestW21_SwitchOnWithoutPasswordAsksForItAtOnce`; four `sharing_*` fixtures regenerated on purpose. +- **W25 — a failed off-site run speaks to the household.** The page's „Utolsó hiba" no longer shows the raw error + („offbox backup immich: exit status 1"); it is built from the failure classifier (07 §6.7) and the run's result + (`offsiteHouseholdFailure`): the failed apps by their own names — „Nem sikerült a távoli mentés: Immich." (EN „The + remote backup did not work for: Immich."), the shares as „Megosztások" / „Shared folders" — then „A többi alkalmazás + távoli mentése rendben van." (EN „The remote backup of the other apps is fine.") only when at least one app was copied + and none was left out, and „A doboz a következő éjjel újra próbálkozik." (EN „The box tries again the next night.") + only when a nightly run is scheduled and a retry can help. A whole-run failure shows its class sentence, our own keyed + sentence (the quota refusal with its numbers), or the apps with no local unit. The raw text stays in the log and, sanitised, in the + operator's event (unchanged). NOT the brief's „holnap éjjel": a run pressed by hand in the day retries THAT night, + and „próbálja" is the R-516 formal-form stem. `TestW25_FailedOffsiteRunSpeaksToTheHousehold`. +- **W28 — the share restore no longer names the controller's own drop-zone share.** „beolvasas" (R-75, the network + drop-in folder, on the system drive) is always outside a data-drive root, so the household read „Nem állítható + vissza (a mappa nincs élő adatmeghajtón): beolvasas." about a share it never made. It is skipped with an INFO log + line; the controller re-creates its definition while sharing is on. Any other refused share is still named. + `TestW28_TheSystemDropZoneIsNotNamedAsRefused`. ## 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/README.md b/controller/README.md index cb5160e..5dc88e8 100644 --- a/controller/README.md +++ b/controller/README.md @@ -4009,6 +4009,10 @@ container recreation. Built by `controller/scripts/build-samba-image.sh` (never - `SMBShare{Name, Path, ReadOnly, Offsite, CreatedAt}` — the share registry. - **The SMB password is NEVER persisted.** Only `UserSet` (a boolean) is stored; the secret lives in the container's passdb, applied via `smbpasswd` on STDIN. +- **Switched on with no password (W21, unreleased):** the service is never deployed until `UserSet` (reconcile + skips it). The switch-on answers „Beállítás mentve. A megosztás akkor indul el, amikor beállítod a megosztási + jelszót." and starts no bring-up job; the page shows „Állíts be egy megosztási jelszót — enélkül a megosztás nem + indul el." with a link to the password form (focused). #### Rendering + lifecycle (`internal/infra/samba.go`, `internal/stacks/samba.go`) diff --git a/controller/internal/backup/offbox.go b/controller/internal/backup/offbox.go index c9c504b..7b6676d 100644 --- a/controller/internal/backup/offbox.go +++ b/controller/internal/backup/offbox.go @@ -271,6 +271,66 @@ func offsiteFailureMessage(t *settings.OffboxTarget, err error, dur time.Duratio return fmt.Sprintf("%s (%s): %s", head, dur.Round(time.Second), sanitiseOffsiteErrorFor(t, err)) } +// offsiteSharesName marks the shares source in offboxRunResult.failedApps; offsiteHouseholdFailure names it in +// the household's language. +const offsiteSharesName = SharesPseudoStack + +// offsiteHouseholdFailure is the sentence the household reads on the „Távoli mentés" page after a failed run (W25, +// rehearsal 2026-10-20 — it read „Utolsó hiba: offbox backup immich: exit status 1"). Built from the failure +// classifier (07 §6.7) and the run's own result, in the box's language, with NO raw error text: that goes to the +// log and, sanitised, to the operator's event (OffsiteFailureMessage), unchanged. +// +// - apps failed while the run went on: they are named; „the other apps are fine" only when at least one app was +// copied and no other app was left out or left incomplete; +// - otherwise our own keyed sentence when the error carries one (the quota refusal, with its numbers), the apps +// with no local unit when there was nothing to send, or the run's cause, one sentence per class +// (note.offsite.fail_); +// - „the box tries again the next night" only when a nightly run will come AND a retry can help (not for a full +// store, a lost key, or nothing to send — those need a person). +// +// Pinned by TestW25_FailedOffsiteRunSpeaksToTheHousehold. +func offsiteHouseholdFailure(lang string, runErr error, res offboxRunResult, nightly bool) string { + class := ClassifyOffsiteFailure(runErr) + var parts []string + retry := false + if len(res.failedApps) > 0 { + names := make([]string, 0, len(res.failedApps)) + for _, n := range res.failedApps { + if n == offsiteSharesName { + n = util.Text(lang, "note.offsite.shares_name") + } + names = append(names, n) + } + parts = append(parts, util.Text(lang, "note.offsite.fail_apps", strings.Join(names, ", "))) + if res.backedUp > 0 && len(res.missingUnprotected) == 0 && len(res.mandatoryGaps) == 0 { + parts = append(parts, util.Text(lang, "note.offsite.others_ok")) + } + retry = class != OffsiteFailQuota && class != OffsiteFailOrphaned + } else if msg, ok := util.AsMsg(runErr); ok { + // Our own sentence (the quota refusal with its numbers and its next step): already the household's. + parts = append(parts, msg.Text(lang)) + retry = class == OffsiteFailTransport || class == OffsiteFailLocked || class == OffsiteFailUnknown + } else if class == OffsiteFailNoUnits && len(res.missing) > 0 { + parts = append(parts, util.Text(lang, "note.offsite.no_local_unit", strings.Join(res.missing, ", "))) + } else { + key := map[OffsiteFailureClass]string{ + OffsiteFailQuota: "note.offsite.fail_quota", OffsiteFailOrphaned: "note.offsite.fail_orphaned", + OffsiteFailNoRepo: "note.offsite.fail_no_repo", OffsiteFailNoUnits: "note.offsite.fail_no_units", + OffsiteFailTransport: "note.offsite.fail_transport", OffsiteFailUnknown: "note.offsite.fail_unknown", + OffsiteFailLocked: "note.offsite.fail_locked", + }[class] + if key == "" { + key = "note.offsite.fail_head" + } + parts = append(parts, util.Text(lang, key)+".") + retry = class == OffsiteFailTransport || class == OffsiteFailLocked || class == OffsiteFailUnknown + } + if retry && nightly { + parts = append(parts, util.Text(lang, "note.offsite.retry_next_night")) + } + return strings.Join(parts, " ") +} + func defaultOffboxSSH(ctx context.Context, host, user string, port int, keyPath, knownHosts, remoteCmd string) ([]byte, error) { if port == 0 { port = 22 @@ -1099,6 +1159,10 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error // everything else that touches settings.json on that box. Written this way once and caught by the // suite timing out at 25 minutes; TestNoteHelpersAreNotCalledUnderTheSettingsLock keeps it fixed. lang := m.boxLang() + householdNote := "" + if runErr != nil { + householdNote = offsiteHouseholdFailure(lang, runErr, runResult, t != nil && t.Enabled && t.Schedule == "daily") + } if perr := m.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.LastRun = time.Now().UTC().Format(time.RFC3339) // R-100: LastRun above records the ATTEMPT; this records the RESULT. The hub's staleness @@ -1115,7 +1179,7 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error o.LastWarningKind = "" } else if runErr != nil { o.LastStatus = "error" - o.LastError = runErr.Error() + o.LastError = householdNote // W25: the household's sentence; the raw text is in the log and the operator's event o.LastWarning = "" o.LastWarningKind = "" } else { @@ -1371,6 +1435,10 @@ type offboxRunResult struct { // longer installed is named but does not, so a removed app cannot leave the box amber forever. missingUnprotected []string missingNotDeployed []string + // failedApps (W25) names, for the household, every app whose push FAILED while the run went on — the + // display name from the app's recovery unit (the stack name when the unit has none), and the shares + // source under its own name. firstErr still carries the first raw error for the log and the operator. + failedApps []string } // stackDeployed reports whether the stack is currently deployed on this box. Used only to classify a @@ -1481,6 +1549,11 @@ func (m *Manager) runOffboxInternal(ctx context.Context, apps, base, env []strin if firstErr == nil { firstErr = fmt.Errorf("offbox backup %s: %w", stack, berr) } + display := stack + if man := readManifest(UnitManifestFile(src)); man != nil && strings.TrimSpace(man.DisplayName) != "" { + display = man.DisplayName + } + res.failedApps = append(res.failedApps, display) continue } res.backedUp++ @@ -1517,6 +1590,9 @@ func (m *Manager) runOffboxInternal(ctx context.Context, apps, base, env []strin if sharesRes.ran { res.sharesBackedUp = true } + if sharesErr != nil { + res.failedApps = append(res.failedApps, offsiteSharesName) + } if sharesErr != nil && firstErr == nil { firstErr = sharesErr } diff --git a/controller/internal/backup/shares_restore.go b/controller/internal/backup/shares_restore.go index c8e02a5..040dcba 100644 --- a/controller/internal/backup/shares_restore.go +++ b/controller/internal/backup/shares_restore.go @@ -241,6 +241,16 @@ func (m *Manager) PlaceSharesRestore(ctx context.Context) (SharesRestoreResult, // THE PREFIX ASSERT. The snapshot's path is untrusted layout input; a destination that is not // strictly inside a live registered root is refused, never created. if !m.liveShareRootOK(sh.Path) { + // W28 (rehearsal 2026-10-20): the controller's OWN drop-zone share („beolvasas", R-75) lives on the + // SYSTEM drive, never under a data-drive root, so it is always refused here — and the household read + // „Nem állítható vissza (…): beolvasas." about a share it never made. Its definition comes back by + // itself (ensureImportShare, whenever sharing is on) and its folder is an inbox apps empty, so it is + // skipped without a word to the household; the refusal of any OTHER share is unchanged. + // Pinned by TestW28_TheSystemDropZoneIsNotNamedAsRefused. + if sh.System { + m.logger.Printf("[INFO] [shares] restore skipped for the system drop-zone share %s — the controller re-creates it while sharing is on", sh.Name) + continue + } m.logger.Printf("[WARN] [shares] restore refused for %s — destination is not under a live storage root", sh.Name) res.Refused = append(res.Refused, sh.Name) continue diff --git a/controller/internal/backup/shares_restore_test.go b/controller/internal/backup/shares_restore_test.go index 1d0d67a..f563f0b 100644 --- a/controller/internal/backup/shares_restore_test.go +++ b/controller/internal/backup/shares_restore_test.go @@ -266,3 +266,40 @@ func TestSharesRestoreFlashMessage(t *testing.T) { // jsonMarshalIndent is a tiny indirection so the test file needs no direct encoding/json import // beyond this one helper. func jsonMarshalIndent(v any) ([]byte, error) { return json.MarshalIndent(v, "", " ") } + +// W28 (rehearsal 2026-10-20): the share restore said „Nem állítható vissza (a mappa nincs élő adatmeghajtón): +// beolvasas." — „beolvasas" is the controller's own drop-zone share (R-75, System), which lives on the system drive +// and is re-created by the controller while sharing is on. It is skipped without naming it; a household share outside +// every live root is still refused and named. +func TestW28_TheSystemDropZoneIsNotNamedAsRefused(t *testing.T) { + env := newSharesEnv(t, "hdd_1") + dropZone := filepath.Join(env.tmp, "system", "felhom-data", "userdata", "import") + outside := filepath.Join(env.tmp, "kivul", "csaladi") + mf := SharesManifest{ + Version: sharesManifestVersion, ServerName: "FELHOM", + Shares: []settings.SMBShare{ + {Name: settings.SystemImportShareName, Path: dropZone, System: true, CreatedAt: "2026-07-18T00:00:00Z"}, + {Name: "csaladi", Path: outside, Offsite: true, CreatedAt: "2026-07-18T00:00:00Z"}, + }, + } + seedSharesScratch(t, env, mf, map[string]string{filepath.Join(dropZone, "scan.pdf"): "X"}) + var passdbCalls int + wirePlaceSeams(env, &passdbCalls) + res, err := env.m.PlaceSharesRestore(t.Context()) + if err != nil { + t.Fatal(err) + } + msg := res.FlashMessage() + if strings.Contains(msg, settings.SystemImportShareName) { + t.Errorf("the household was told about the controller's drop-zone share: %q", msg) + } + if len(res.Refused) != 1 || res.Refused[0] != "csaladi" || !strings.Contains(msg, "csaladi") { + t.Errorf("a household share outside every live root must still be refused and named: %+v %q", res.Refused, msg) + } + if _, sErr := os.Stat(filepath.Join(dropZone, "scan.pdf")); !os.IsNotExist(sErr) { + t.Error("the drop-zone's files were placed — the skip must write nothing") + } + if len(env.sett.GetSMBShares()) != 0 { + t.Errorf("a skipped share must not enter the registry here: %+v", env.sett.GetSMBShares()) + } +} diff --git a/controller/internal/backup/w25_offsite_household_test.go b/controller/internal/backup/w25_offsite_household_test.go new file mode 100644 index 0000000..c0845b8 --- /dev/null +++ b/controller/internal/backup/w25_offsite_household_test.go @@ -0,0 +1,69 @@ +package backup + +import ( + "context" + "encoding/json" + "errors" + "os" + "strings" + "testing" + "time" +) + +// W25 (rehearsal 2026-10-20): a failed off-site run showed the household „Utolsó hiba: offbox backup immich: exit +// status 1". The household now reads which app failed (its own name), that the other apps are fine when they are, +// and that the box tries again the next night; the raw text stays in the log and reaches the operator's event. +func TestW25_FailedOffsiteRunSpeaksToTheHousehold(t *testing.T) { + m, sett := newOffboxManager(t) + usb := t.TempDir() + addSchedulablePath(t, sett, usb) + ns := m.namespaceRoot(usb) + writeUnit(t, ns, "immich", "2026-10-20T00:00:00Z") + writeUnit(t, ns, "mealie", "2026-10-20T00:00:00Z") + b, _ := json.Marshal(RecoveryManifest{AppName: "immich", DisplayName: "Immich Fotók", CreatedAt: "2026-10-20T00:00:00Z"}) + if err := os.WriteFile(RecoveryUnitManifestPath(ns, "immich"), b, 0o644); err != nil { + t.Fatal(err) + } + _ = sett.SetAppOffbox("immich", true) + _ = sett.SetAppOffbox("mealie", true) + rr := &recordingOffboxRunner{backupErr: map[string]error{"immich": errors.New("exit status 1")}} + m.SetOffboxRunner(rr.run) + var operatorErr error + m.SetOffboxNotify(func(_ time.Duration, _ int, err error) { operatorErr = err }) + if err := m.RunOffboxBackup(context.Background()); err == nil { + t.Fatal("a run with a failed app must report an error") + } + got := sett.GetOffboxTarget().LastError + for _, want := range []string{"Nem sikerült a távoli mentés: Immich Fotók.", "A többi alkalmazás távoli mentése rendben van.", + "A doboz a következő éjjel újra próbálkozik."} { + if !strings.Contains(got, want) { + t.Errorf("the household's note lacks %q: %q", want, got) + } + } + for _, raw := range []string{"exit status", "offbox backup", "immich:"} { + if strings.Contains(got, raw) { + t.Errorf("raw technical text %q reached the household's note: %q", raw, got) + } + } + // the operator's event keeps the technical detail, sanitised + if op := m.OffsiteFailureMessage(operatorErr, time.Second); !strings.Contains(op, "offbox backup immich: exit status 1") { + t.Errorf("the operator's event lost the technical detail: %q", op) + } + + // English, and the cases where the other apps are NOT fine or no night comes + res := offboxRunResult{backedUp: 1, failedApps: []string{"Immich", offsiteSharesName}} + runErr := errors.New("offbox backup immich: exit status 1") + en := offsiteHouseholdFailure("en", runErr, res, true) + if en != "The remote backup did not work for: Immich, Shared folders. The remote backup of the other apps is fine. The box tries again the next night." { + t.Errorf("en: %q", en) + } + res.missingUnprotected = []string{"paperless"} + if hu := offsiteHouseholdFailure("hu", runErr, res, false); strings.Contains(hu, "többi") || strings.Contains(hu, "éjjel") { + t.Errorf("with an app left out the others are not all fine, and with no nightly run there is no retry: %q", hu) + } + // a whole-run failure: its class sentence, never the raw text + tr := offsiteHouseholdFailure("hu", errors.New("ssh: connect to host x port 23: connection refused"), offboxRunResult{}, true) + if tr != "A távoli tárhely nem érhető el (hálózat vagy bejelentkezés). A doboz a következő éjjel újra próbálkozik." { + t.Errorf("transport: %q", tr) + } +} diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index b1ac7bc..941d645 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -1599,6 +1599,7 @@ "flash.sharing.save_failed": "That could not be saved.", "flash.sharing.saved": "Setting saved.", "flash.sharing.saved_preparing": "Setting saved. The sharing service is being prepared…", + "flash.sharing.saved_needs_password": "Setting saved. Sharing starts when you set the sharing password.", "flash.tier2.app_email_off": "Email sending is off for this app.", "flash.tier2.app_email_on": "Email sending is on for this app.", "flash.tier2.save_failed": "The setting could not be saved.", @@ -1844,6 +1845,10 @@ "note.offsite.fail_quota": "The remote backup did not fit inside its storage limit", "note.offsite.fail_transport": "The remote store cannot be reached (network or sign-in)", "note.offsite.fail_unknown": "The remote backup failed for an unknown reason", + "note.offsite.fail_apps": "The remote backup did not work for: %s.", + "note.offsite.others_ok": "The remote backup of the other apps is fine.", + "note.offsite.retry_next_night": "The box tries again the next night.", + "note.offsite.shares_name": "Shared folders", "note.offsite.no_local_unit": "These apps did NOT go into the remote backup, because they have no local backup unit yet: %s. The next backup usually makes one — if they are still listed after the second run, tell your operator.", "note.offsite.not_installed": "These apps are selected for remote backup but are not installed, so they cannot be backed up: %s. If you no longer need them, clear their selection on the Remote backup page.", "note.offsite.quota_partial": "Warning: because of the storage limit, %d apps got only a settings and database backup: %s.", @@ -2189,6 +2194,8 @@ "sharing.egy_kozos_jelszo_tartozik_a": "The household has one shared password. When connecting, use this as the username:", "sharing.egyes_halozatokon_a_gep_kesve": "On some networks the machine appears late in the device list. Typing the", "sharing.eloszor_adj_meg_jelszot": "set a password first", + "sharing.needs_password_why": "Set a sharing password. Without it, sharing does not start.", + "sharing.needs_password_link": "Set the password", "sharing.ezen_a_neven_jelenik_meg": "The machine appears on the network under this name (", "sharing.ezt_valasztom": "Choose this", "sharing.felhomentes": "Cloud backup", diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index edf8c44..7531451 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -1590,6 +1590,7 @@ "flash.sharing.save_failed": "A mentés nem sikerült.", "flash.sharing.saved": "Beállítás mentve.", "flash.sharing.saved_preparing": "Beállítás mentve. A megosztási szolgáltatás előkészítése folyamatban…", + "flash.sharing.saved_needs_password": "Beállítás mentve. A megosztás akkor indul el, amikor beállítod a megosztási jelszót.", "flash.tier2.app_email_off": "Email-küldés kikapcsolva ennél az alkalmazásnál.", "flash.tier2.app_email_on": "Email-küldés bekapcsolva ennél az alkalmazásnál.", "flash.tier2.save_failed": "A beállítás mentése nem sikerült.", @@ -1832,6 +1833,10 @@ "note.offsite.fail_quota": "A távoli mentés nem fért el a tárhelykereten belül", "note.offsite.fail_transport": "A távoli tárhely nem érhető el (hálózat vagy bejelentkezés)", "note.offsite.fail_unknown": "A távoli mentés ismeretlen okból nem sikerült", + "note.offsite.fail_apps": "Nem sikerült a távoli mentés: %s.", + "note.offsite.others_ok": "A többi alkalmazás távoli mentése rendben van.", + "note.offsite.retry_next_night": "A doboz a következő éjjel újra próbálkozik.", + "note.offsite.shares_name": "Megosztások", "note.offsite.no_local_unit": "Ezek az alkalmazások NEM kerültek be a távoli mentésbe, mert még nincs helyi mentési egységük: %s. A következő mentés általában már elkészíti — ha a második futás után is itt szerepelnek, szólj az üzemeltetőnek.", "note.offsite.not_installed": "Ezek az alkalmazások ki vannak jelölve távoli mentésre, de nincsenek telepítve, ezért nem menthetők: %s. Ha már nincs rájuk szükséged, vedd ki a kijelölésüket a Távoli mentés oldalon.", "note.offsite.quota_partial": "Figyelmeztetés: a tárhelykeret miatt %d alkalmazásnál csak konfiguráció- és adatbázis-mentés készült: %s.", @@ -2177,6 +2182,8 @@ "sharing.egy_kozos_jelszo_tartozik_a": "Egy közös jelszó tartozik a háztartáshoz. A csatlakozáskor felhasználónévnek add meg:", "sharing.egyes_halozatokon_a_gep_kesve": "Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A", "sharing.eloszor_adj_meg_jelszot": "először adj meg jelszót", + "sharing.needs_password_why": "Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el.", + "sharing.needs_password_link": "Jelszó beállítása", "sharing.ezen_a_neven_jelenik_meg": "Ezen a néven jelenik meg a gép a hálózaton (", "sharing.ezt_valasztom": "Ezt választom", "sharing.felhomentes": "Felhőmentés", diff --git a/controller/internal/web/sharing_handlers.go b/controller/internal/web/sharing_handlers.go index 9fd3470..5168b1d 100644 --- a/controller/internal/web/sharing_handlers.go +++ b/controller/internal/web/sharing_handlers.go @@ -296,6 +296,16 @@ func (s *Server) sharingEnableHandler(w http.ResponseWriter, r *http.Request) { s.logger.Printf("[WARN] [sharing] could not ensure the import share: %v", err) } + // W21 (rehearsal 2026-10-20): with no household password the service is never deployed (reconcile + // skips it: „deploy skipped — household SMB password not set yet"), yet the flash said the service was + // being prepared and the page stayed „áll" for ever. The answer now says it waits for the password, and + // the page asks for it (sharing.html, the needs-password alert). Nothing to prepare, so no job. + // Pinned by TestW21_SwitchOnWithoutPasswordAsksForItAtOnce. + if !s.settings.GetSMBSettings().UserSet { + s.logger.Printf("[INFO] [sharing] sharing switched on; the service waits for the household password") + sharingRedirect(w, r, "flash.sharing.saved_needs_password") + return + } // v0.147.0 (4b): the bring-up runs DETACHED and the page polls it. Synchronously it was a form // post that hung for minutes on a first-enable image pull and then flashed „Beállítás mentve." // regardless of whether anything actually came up. diff --git a/controller/internal/web/templates/sharing.html b/controller/internal/web/templates/sharing.html index 93790ef..84736f1 100644 --- a/controller/internal/web/templates/sharing.html +++ b/controller/internal/web/templates/sharing.html @@ -53,6 +53,12 @@ {{T "sharing.eloszor_adj_meg_jelszot"}} {{end}} + {{- /* W21 (rehearsal 2026-10-20): the service waits for the password; the page says so at once. */}} + {{- if and .SMBEnabled (not .SMBUserSet)}} +
+ {{T "sharing.needs_password_why"}} {{T "sharing.needs_password_link"}} +
+ {{- end}}
{{T "sharing.egyes_halozatokon_a_gep_kesve"}} \\{{.SMBServerName}} {{T "sharing.cim_beirasa_a_cimsorba_ilyenkor"}} @@ -97,7 +103,7 @@
+ autocomplete="new-password" minlength="8" required{{if and .SMBEnabled (not .SMBUserSet)}} autofocus{{end}}>
diff --git a/controller/internal/web/testdata/i18n_parity/sharing_error_skipped.html b/controller/internal/web/testdata/i18n_parity/sharing_error_skipped.html index 24dc113..a3f3af8 100644 --- a/controller/internal/web/testdata/i18n_parity/sharing_error_skipped.html +++ b/controller/internal/web/testdata/i18n_parity/sharing_error_skipped.html @@ -221,6 +221,9 @@ először adj meg jelszót
+
+ Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. Jelszó beállítása +
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A \\FELHOM cím beírása a címsorba ilyenkor is működik. @@ -253,7 +256,7 @@
+ autocomplete="new-password" minlength="8" required autofocus>
diff --git a/controller/internal/web/testdata/i18n_parity/sharing_full.html b/controller/internal/web/testdata/i18n_parity/sharing_full.html index 478224c..f9fd93c 100644 --- a/controller/internal/web/testdata/i18n_parity/sharing_full.html +++ b/controller/internal/web/testdata/i18n_parity/sharing_full.html @@ -223,6 +223,9 @@ először adj meg jelszót
+
+ Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. Jelszó beállítása +
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A \\FELHOM cím beírása a címsorba ilyenkor is működik. @@ -263,7 +266,7 @@
+ autocomplete="new-password" minlength="8" required autofocus>
diff --git a/controller/internal/web/testdata/i18n_parity/sharing_no_offsite_yet.html b/controller/internal/web/testdata/i18n_parity/sharing_no_offsite_yet.html index 567a93b..0216595 100644 --- a/controller/internal/web/testdata/i18n_parity/sharing_no_offsite_yet.html +++ b/controller/internal/web/testdata/i18n_parity/sharing_no_offsite_yet.html @@ -221,6 +221,9 @@ először adj meg jelszót
+
+ Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. Jelszó beállítása +
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A \\FELHOM cím beírása a címsorba ilyenkor is működik. @@ -253,7 +256,7 @@
+ autocomplete="new-password" minlength="8" required autofocus>
diff --git a/controller/internal/web/testdata/i18n_parity/sharing_other_none.html b/controller/internal/web/testdata/i18n_parity/sharing_other_none.html index 7bd05cb..8fcd8ec 100644 --- a/controller/internal/web/testdata/i18n_parity/sharing_other_none.html +++ b/controller/internal/web/testdata/i18n_parity/sharing_other_none.html @@ -221,6 +221,9 @@ először adj meg jelszót
+
+ Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. Jelszó beállítása +
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A \\FELHOM cím beírása a címsorba ilyenkor is működik. @@ -253,7 +256,7 @@
+ autocomplete="new-password" minlength="8" required autofocus>
diff --git a/controller/internal/web/w21_sharing_password_test.go b/controller/internal/web/w21_sharing_password_test.go new file mode 100644 index 0000000..9c8df26 --- /dev/null +++ b/controller/internal/web/w21_sharing_password_test.go @@ -0,0 +1,57 @@ +package web + +import ( + "html" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +// W21 (rehearsal 2026-10-20): „switch on" with no household SMB password answered „Beállítás mentve. A megosztási +// szolgáltatás előkészítése folyamatban…" — but reconcile never deploys without the password („deploy skipped — +// household SMB password not set yet") and the page stayed „Állapot: áll" for ever. The answer and the page now ask +// for the password at once and say why the service waits; with a password set, the bring-up is unchanged. +func TestW21_SwitchOnWithoutPasswordAsksForItAtOnce(t *testing.T) { + s := i18nTestServer(t) + enable := func() string { + r := httptest.NewRequest(http.MethodPost, "/sharing/enable", strings.NewReader(url.Values{"enabled": {"on"}}.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + s.sharingEnableHandler(w, r) + if w.Code != http.StatusSeeOther { + t.Fatalf("enable: %d", w.Code) + } + return w.Header().Get("Location") + } + loc := enable() + if !strings.Contains(loc, "flash.sharing.saved_needs_password") { + t.Fatalf("switch-on without a password must say the service waits for it: %q", loc) + } + for lang, want := range map[string][2]string{ + "hu": {"A megosztás akkor indul el, amikor beállítod a megosztási jelszót.", "Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el."}, + "en": {"Sharing starts when you set the sharing password.", "Set a sharing password. Without it, sharing does not start."}, + } { + r := httptest.NewRequest(http.MethodGet, loc, nil) + r.AddCookie(&http.Cookie{Name: langCookieName, Value: lang}) + w := httptest.NewRecorder() + s.sharingPageHandler(w, r) + page := html.UnescapeString(w.Body.String()) + if !strings.Contains(page, want[0]) || !strings.Contains(page, want[1]) || !strings.Contains(page, `id="smb-needs-password"`) { + t.Errorf("%s: the page does not ask for the password at once", lang) + } + if strings.Contains(page, "szolgáltatás előkészítése folyamatban") || strings.Contains(page, "is being prepared") { + t.Errorf("%s: the page still says the service is being prepared", lang) + } + } + // with the password set the page asks for nothing (the bring-up path is the existing one, pinned elsewhere) + if err := s.settings.SetSMBUserSet(true); err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + s.sharingPageHandler(w, httptest.NewRequest(http.MethodGet, "/sharing", nil)) + if strings.Contains(w.Body.String(), `id="smb-needs-password"`) { + t.Fatal("the needs-password alert shows although a password is set") + } +} diff --git a/controller/scripts/i18n_go_keys.json b/controller/scripts/i18n_go_keys.json index 21cb635..28ca40b 100644 --- a/controller/scripts/i18n_go_keys.json +++ b/controller/scripts/i18n_go_keys.json @@ -305,7 +305,12 @@ "flash.kept.not_found": "BORN AS A KEY (D25, operator ruling 2026-10-21; R-1175/R-1181) -- kept second-drive copies: their list line, reasons, the emptied-folder note and the „Eltávolítás” flashes. Pinned by internal/web/kept_copies_test.go and internal/backup/r1175_shares_kept_test.go.", "flash.kept.changed": "BORN AS A KEY (D25, operator ruling 2026-10-21; R-1175/R-1181) -- kept second-drive copies: their list line, reasons, the emptied-folder note and the „Eltávolítás” flashes. Pinned by internal/web/kept_copies_test.go and internal/backup/r1175_shares_kept_test.go.", "flash.kept.remove_failed": "BORN AS A KEY (D25, operator ruling 2026-10-21; R-1175/R-1181) -- kept second-drive copies: their list line, reasons, the emptied-folder note and the „Eltávolítás” flashes. Pinned by internal/web/kept_copies_test.go and internal/backup/r1175_shares_kept_test.go.", - "err.stacks.memory_overcommit_slow": "BORN AS A KEY (R-1192, 2026-10-21) -- replaces err.stacks.memory_overcommit_warning, whose second sentence („Normál használat mellett ez nem okoz problémát.”) was false: the rehearsal's 8 GB box froze for an hour under its first uploads (felhom.eu audits/fixes-2026-10-21/P4-freeze/). Pinned by TestR575_OvercommitWarningFollowsTheLanguage." + "err.stacks.memory_overcommit_slow": "BORN AS A KEY (R-1192, 2026-10-21) -- replaces err.stacks.memory_overcommit_warning, whose second sentence („Normál használat mellett ez nem okoz problémát.”) was false: the rehearsal's 8 GB box froze for an hour under its first uploads (felhom.eu audits/fixes-2026-10-21/P4-freeze/). Pinned by TestR575_OvercommitWarningFollowsTheLanguage.", + "flash.sharing.saved_needs_password": "BORN AS A KEY (W21, rehearsal word table, 2026-10-21) -- the switch-on answer when no household SMB password is set: the service waits for it (reconcile skips the deploy). A NEW sentence, never a Go literal. Pinned by TestW21_SwitchOnWithoutPasswordAsksForItAtOnce.", + "note.offsite.fail_apps": "BORN AS A KEY (W25, rehearsal word table, 2026-10-21) -- the household's sentence after a failed off-site run, built from the failure classifier (07 §6.7) and the run's result; the raw error stays in the log and the operator's event. A NEW sentence, never a Go literal. Pinned by TestW25_FailedOffsiteRunSpeaksToTheHousehold.", + "note.offsite.others_ok": "BORN AS A KEY (W25, rehearsal word table, 2026-10-21) -- the household's sentence after a failed off-site run, built from the failure classifier (07 §6.7) and the run's result; the raw error stays in the log and the operator's event. A NEW sentence, never a Go literal. Pinned by TestW25_FailedOffsiteRunSpeaksToTheHousehold.", + "note.offsite.retry_next_night": "BORN AS A KEY (W25, rehearsal word table, 2026-10-21) -- the household's sentence after a failed off-site run, built from the failure classifier (07 §6.7) and the run's result; the raw error stays in the log and the operator's event. A NEW sentence, never a Go literal. Pinned by TestW25_FailedOffsiteRunSpeaksToTheHousehold.", + "note.offsite.shares_name": "BORN AS A KEY (W25, rehearsal word table, 2026-10-21) -- the household's sentence after a failed off-site run, built from the failure classifier (07 §6.7) and the run's result; the raw error stays in the log and the operator's event. A NEW sentence, never a Go literal. Pinned by TestW25_FailedOffsiteRunSpeaksToTheHousehold." }, "flash.share.already_on": "A megosztás már be van kapcsolva.", "flash.share.enable_failed": "A megosztás bekapcsolása nem sikerült.",