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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user