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
gates / gates (push) Successful in 1m40s
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user