controller: zero-toggle honesty — a configured+escrowed offbox with no app selected shows 'Nincs távoli mentésre jelölt alkalmazás…' on the toggle list, and a zero-toggle run reports 'Sikeres — nincs mentésre jelölt alkalmazás' instead of bare success

Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
2026-07-13 08:27:52 +02:00
parent a00afcc79d
commit b482860c03
4 changed files with 57 additions and 1 deletions
+6
View File
@@ -441,6 +441,12 @@ func (m *Manager) RunOffboxBackup(ctx context.Context) error {
o.LastError = "" o.LastError = ""
o.SnapshotCount = snapshots o.SnapshotCount = snapshots
var warns []string var warns []string
// Zero-toggle honesty (take-two obs.): a configured target with NOTHING selected reports
// its emptiness instead of a bare success — the customer thinks offsite runs, but nothing
// is covered until at least one app is toggled.
if len(apps) == 0 {
warns = append(warns, "Sikeres — nincs mentésre jelölt alkalmazás")
}
if len(missing) > 0 { if len(missing) > 0 {
warns = append(warns, fmt.Sprintf("Figyelmeztetés: %d alkalmazásnak nincs elérhető mentése, ezek kimaradtak: %s", warns = append(warns, fmt.Sprintf("Figyelmeztetés: %d alkalmazásnak nincs elérhető mentése, ezek kimaradtak: %s",
len(missing), strings.Join(missing, ", "))) len(missing), strings.Join(missing, ", ")))
+37
View File
@@ -194,6 +194,43 @@ func TestOffbox_RunFailsFastAndAlerts(t *testing.T) {
} }
} }
// Zero-toggle honesty (take-two obs., v0.123.0): a run with a configured+escrowed target but ZERO
// toggled apps must stay status=ok yet report "Sikeres — nincs mentésre jelölt alkalmazás" instead
// of a bare success. Red-proof: drop the len(apps)==0 warns branch → the LastWarning assertion fails.
func TestOffbox_ZeroToggledRunReportsEmptiness(t *testing.T) {
m, sett := newOffboxManager(t)
m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
if contains(args, "cat") && contains(args, "config") {
return []byte(`{"version":2}`), nil // repo exists
}
return []byte(`[]`), nil
})
// No SetAppOffbox call — zero apps toggled.
if err := m.RunOffboxBackup(context.Background()); err != nil {
t.Fatalf("zero-toggled run must not error: %v", err)
}
st := sett.GetOffboxTarget()
if st.LastStatus != "ok" {
t.Fatalf("zero-toggled run status = %q, want ok (emptiness is honesty, not failure)", st.LastStatus)
}
if !strings.Contains(st.LastWarning, "Sikeres — nincs mentésre jelölt alkalmazás") {
t.Fatalf("zero-toggled run must report its emptiness, LastWarning = %q", st.LastWarning)
}
// Companion negative: with one toggled app captured, the emptiness copy must NOT appear.
nsRoot := m.AppNamespaceRoot("rallly")
if err := os.MkdirAll(filepath.Join(RecoveryUnitPath(nsRoot, "rallly"), "db-dumps"), 0o755); err != nil {
t.Fatal(err)
}
_ = sett.SetAppOffbox("rallly", true)
if err := m.RunOffboxBackup(context.Background()); err != nil {
t.Fatalf("toggled run must not error: %v", err)
}
if st := sett.GetOffboxTarget(); strings.Contains(st.LastWarning, "nincs mentésre jelölt") {
t.Fatalf("a covered run must not carry the emptiness copy, LastWarning = %q", st.LastWarning)
}
}
// TestOffbox_RepoIdempotent: when the repo exists (cat config succeeds), ensure must NOT init. // TestOffbox_RepoIdempotent: when the repo exists (cat config succeeds), ensure must NOT init.
func TestOffbox_RepoIdempotent(t *testing.T) { func TestOffbox_RepoIdempotent(t *testing.T) {
m, sett := newOffboxManager(t) m, sett := newOffboxManager(t)
+11 -1
View File
@@ -671,7 +671,17 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
offboxTgt := s.settings.GetOffboxTarget() offboxTgt := s.settings.GetOffboxTarget()
data["Offbox"] = offboxTgt data["Offbox"] = offboxTgt
data["OffboxConfigured"] = s.backupMgr.OffboxConfigured() data["OffboxConfigured"] = s.backupMgr.OffboxConfigured()
data["OffboxApps"] = s.buildOffboxApps() offboxApps := s.buildOffboxApps()
data["OffboxApps"] = offboxApps
// Zero-toggle hint (take-two obs.): configured + escrowed but no app selected — nothing is
// actually covered by the offsite leg until the customer toggles at least one.
offboxToggled := 0
for _, a := range offboxApps {
if a.Enabled {
offboxToggled++
}
}
data["OffboxToggledCount"] = offboxToggled
// SLICE 4 soft-quota usage bar (rendered only when a quota is set — shared model). // SLICE 4 soft-quota usage bar (rendered only when a quota is set — shared model).
data["OffboxQuotaPct"] = backup.OffboxQuotaPercent(offboxTgt) data["OffboxQuotaPct"] = backup.OffboxQuotaPercent(offboxTgt)
} else { } else {
@@ -169,6 +169,9 @@
</form> </form>
</div> </div>
<h4 style="margin-top:1.25rem">Mely alkalmazások mentődnek a távoli tárolóra?</h4> <h4 style="margin-top:1.25rem">Mely alkalmazások mentődnek a távoli tárolóra?</h4>
{{if and .OffboxApps (eq .Offbox.EscrowState "escrowed") (eq .OffboxToggledCount 0)}}
<p class="form-hint">Nincs távoli mentésre jelölt alkalmazás — jelölj ki legalább egyet.</p>
{{end}}
{{if .OffboxApps}} {{if .OffboxApps}}
<div class="storage-paths-list"> <div class="storage-paths-list">
{{range .OffboxApps}} {{range .OffboxApps}}