W21 sharing switched on without a password asks for it and says why it waits; W25 failed off-site run shows the household a sentence built from the classifier, raw text only to log and operator; W28 share restore skips the controller's drop-zone share; unreleased
gates / gates (push) Successful in 1m40s

This commit is contained in:
2026-10-21 23:19:07 +02:00
parent 28b05d6d68
commit 5776d54901
16 changed files with 330 additions and 7 deletions
+23
View File
@@ -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)
+4
View File
@@ -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`)
+77 -1
View File
@@ -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_<class>);
// - „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
}
@@ -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
@@ -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())
}
}
@@ -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)
}
}
+7
View File
@@ -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",
+7
View File
@@ -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",
@@ -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.
@@ -53,6 +53,12 @@
<span class="badge badge-warn">{{T "sharing.eloszor_adj_meg_jelszot"}}</span>
{{end}}
</div>
{{- /* W21 (rehearsal 2026-10-20): the service waits for the password; the page says so at once. */}}
{{- if and .SMBEnabled (not .SMBUserSet)}}
<div class="alert alert-warning" id="smb-needs-password">
{{T "sharing.needs_password_why"}} <a href="#smb_password">{{T "sharing.needs_password_link"}}</a>
</div>
{{- end}}
<div class="form-hint">
{{T "sharing.egyes_halozatokon_a_gep_kesve"}}
<span class="mono">\\{{.SMBServerName}}</span> {{T "sharing.cim_beirasa_a_cimsorba_ilyenkor"}}
@@ -97,7 +103,7 @@
<div class="form-group">
<label for="smb_password">{{T "sharing.uj_jelszo"}}</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required{{if and .SMBEnabled (not .SMBUserSet)}} autofocus{{end}}>
</div>
<div class="form-group">
<label for="smb_password_confirm">{{T "sharing.jelszo_megerositese"}}</label>
@@ -221,6 +221,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -253,7 +256,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -223,6 +223,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -263,7 +266,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -221,6 +221,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -253,7 +256,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -221,6 +221,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -253,7 +256,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -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")
}
}
+6 -1
View File
@@ -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.",