R-1271 (P2): „use my kept data” never takes a copy dated in the future — a copy more than 10 minutes past the box's clock never counts as the newest (the newest true date wins, tie local); alone it is still offered, said and not recommended. Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
2026-10-25 09:44:56 +01:00
parent 2d71270858
commit bd9f4f7b74
13 changed files with 278 additions and 32 deletions
+19
View File
@@ -22,6 +22,25 @@ brief 2026-10-24, Part 4 (commit only; the release rides the next normal train).
`…OnlyWhenTheAgentWaitsAndAnAppRuns`, `…QBLimitRuleSkipsAHeldLimit`, `…TheTwoAppsReadAndWriteTheirLimit`, `…OnlyWhenTheAgentWaitsAndAnAppRuns`, `…QBLimitRuleSkipsAHeldLimit`, `…TheTwoAppsReadAndWriteTheirLimit`,
`TestLineTestRunOutlastsTheClientTimeout`, `TestStartUpwatchRecoversTheHoldFirst`, `TestUpwatchHoldGetsBothAgentCalls`. `TestLineTestRunOutlastsTheClientTimeout`, `TestStartUpwatchRecoversTheHoldFirst`, `TestUpwatchHoldGetsBothAgentCalls`.
### „Use my kept data" never takes a copy dated in the future (R-1271, P2) — 2026-10-25, commit only
- **The cause, read from the code:** the catalog sweep's Tester4 off-site store held the 2026-10-20 rehearsal box's
copies, dated 2026-10-28 (that box's clock was set ahead). `KeptNewer` took the off-site copy whenever its date was
after the local one's; that date was the other box's snapshot time, uncapped (this box had no data time of its own
for it, `offsiteDataTime`). The `07` §6.5 „another drive" check (`KeptCopyAt`) compares the unit's `HDD_PATH` — a
mount path with the same name on every box — so it is a path check, and it stays one (an old box's data on a new box
is what off-site is for). Immich's 20 minutes of 404 were routing: the copy brought the old box's subdomain
(`photos`); the app was healthy at `photos.<domain>`.
- **Now (`keptPick`, `KeptNewerAt`):** a copy dated more than `KeptClockSkew` (10 minutes) past this box's clock is
future-dated and never the newest; the newest copy with a true date wins (tie → local, own unit before the second
drive). A future-dated copy alone is still offered: the install question carries `use_future` and a sentence („…a
dátuma (…) a jövőben van… Csak akkor válaszd, ha biztosan ez a ti mentésetek."), and draws „use" as the plain button,
not the primary one; the kept-data list adds „(a dátuma a jövőben van…)". The question names the copy as before.
- Tests (each seen failing with the future check off): `TestR1271_AFutureOffsiteCopyNeverBeatsTheOwnCopy`,
`…AFutureCopyAloneIsOfferedButMarked`, `…TheClockSkewAllowance`, api `…AFutureCopyAloneIsSaidAndNotRecommended`;
controls `…AnOlderOffsiteCopyAloneIsChosenAsBefore`, `…ANewerTrueOffsiteCopyStillWins`. The three install-page parity
fixtures carry the two changed script lines (nothing else moved).
### A restore downloads first (D48) — 2026-10-25, commit only ### A restore downloads first (D48) — 2026-10-25, commit only
Operator ruling D48 (2026-10-25 08:38, `09` §3 decision 257): every restore path resolves and pulls every image its Operator ruling D48 (2026-10-25 08:38, `09` §3 decision 257): every restore path resolves and pulls every image its
+9 -1
View File
@@ -20,6 +20,10 @@ type keptChoiceData struct {
UseDesc string `json:"use_desc"` // "" when use is off UseDesc string `json:"use_desc"` // "" when use is off
UseOff string `json:"use_off"` // "" when use is offered UseOff string `json:"use_off"` // "" when use is offered
UseOffered bool `json:"use_offered"` // a database copy exists for these files UseOffered bool `json:"use_offered"` // a database copy exists for these files
// UseFuture / UseWarn (R-1271): the only copy is dated in the future — offered, NOT recommended (the page draws
// „use" as the plain button), and the sentence says why.
UseFuture bool `json:"use_future,omitempty"`
UseWarn string `json:"use_warn,omitempty"`
FreshLabel string `json:"fresh_label"` FreshLabel string `json:"fresh_label"`
FreshDesc string `json:"fresh_desc"` FreshDesc string `json:"fresh_desc"`
NotBacked string `json:"not_backed_up"` NotBacked string `json:"not_backed_up"`
@@ -71,13 +75,17 @@ func (r *Router) keptDataAtInstall(w http.ResponseWriter, req *http.Request, nam
} }
if cp.ok { if cp.ok {
data.UseDesc = r.msgLang(lang, "kept.choice.use.desc_from", r.keptCopyName(lang, cp.c)) data.UseDesc = r.msgLang(lang, "kept.choice.use.desc_from", r.keptCopyName(lang, cp.c))
if cp.c.Future {
data.UseFuture = true
data.UseWarn = r.msgLang(lang, "kept.choice.use.future", localMinute(cp.c.Time))
}
} else { } else {
data.UseOff = r.msgLang(lang, "kept.choice.use_off") data.UseOff = r.msgLang(lang, "kept.choice.use_off")
} }
switch choice { switch choice {
case "": case "":
r.logger.Printf("[INFO] [api] Deploy %s: the drive already holds its old data %v (%d bytes) — asking the household (use offered: %v)", name, old, size, cp.ok) r.logger.Printf("[INFO] [api] Deploy %s: the drive already holds its old data %v (%d bytes) — asking the household (use offered: %v, future-dated: %v)", name, old, size, cp.ok, cp.c.Future)
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: data.Title, Data: data}) writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: data.Title, Data: data})
return true return true
case stacks.KeptChoiceUse: case stacks.KeptChoiceUse:
@@ -0,0 +1,86 @@
package api
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// R-1271 (brief 2026-10-25 Part 4) — a future-dated off-site copy, the ONLY copy of the kept files: still offered (it
// may be the household's), but the question says its date is in the future and does not recommend it. Same rig as
// TestR691 (the restic runner seam; nothing reaches restic, ssh or Docker).
// COMPANION RED-PROOF (REPORT.md): with keptPick ignoring the future date, use_future stays false and this fails.
func TestR1271_AFutureCopyAloneIsSaidAndNotRecommended(t *testing.T) {
dir := t.TempDir()
drive := filepath.Join(dir, "drive")
cfg := &config.Config{}
cfg.Paths.StacksDir = filepath.Join(dir, "stacks")
cfg.Paths.DataDir = filepath.Join(dir, "data")
cfg.Stacks.ComposeCommand = "docker compose"
app := filepath.Join(cfg.Paths.StacksDir, "cloudapp")
for _, d := range []string{app, filepath.Join(drive, "appdata/cloudapp"), cfg.Paths.DataDir} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
_ = os.WriteFile(filepath.Join(app, "docker-compose.yml"), []byte("services:\n cloudapp:\n image: busybox\n volumes:\n - ${HDD_PATH}/appdata/cloudapp:/data\n"), 0o644)
_ = os.WriteFile(filepath.Join(app, ".felhom.yml"), []byte("display_name: Cloud App\n"), 0o644)
_ = os.WriteFile(filepath.Join(drive, "appdata/cloudapp/old.txt"), []byte("old"), 0o644)
m, err := stacks.NewManager(cfg, log.New(io.Discard, "", 0))
if err != nil {
t.Fatal(err)
}
if err := m.ScanStacks(); err != nil {
t.Fatal(err)
}
sett, err := settings.Load(filepath.Join(cfg.Paths.DataDir, "settings.json"), log.New(io.Discard, "", 0))
if err != nil {
t.Fatal(err)
}
if err := sett.SetOffboxTarget(&settings.OffboxTarget{Enabled: true, Host: "nas.local", Port: 22, User: "felhom",
RepoPath: "/srv/repo", Schedule: "daily", EscrowState: "escrowed"}); err != nil {
t.Fatal(err)
}
bm := backup.NewManager(cfg, sett, log.New(io.Discard, "", 0))
if err := bm.WriteOffboxSecrets("KEY", "nas.local ssh-ed25519 AAAA"); err != nil {
t.Fatal(err)
}
snapAt := time.Now().Add(4 * 24 * time.Hour) // the rehearsal box's clock: four days ahead
bm.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
for _, a := range args {
if a == "snapshots" {
return json.Marshal([]map[string]interface{}{{"short_id": "abba81ba", "time": snapAt,
"tags": []string{"cloudapp"}, "paths": []string{drive + "/backups/primary/cloudapp", drive + "/appdata/cloudapp"}}})
}
}
return nil, nil
})
r := &Router{stackMgr: m, backupMgr: bm, logger: log.New(io.Discard, "", 0)}
for lang, want := range map[string]string{"hu": "a jövőben van", "en": "is in the future"} {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/stacks/cloudapp/deploy?lang="+lang, nil)
if !r.keptDataAtInstall(w, req, "cloudapp", drive, "") || w.Code != http.StatusConflict {
t.Fatalf("%s: the choice was not asked (code %d)", lang, w.Code)
}
var body map[string]interface{}
_ = json.Unmarshal(w.Body.Bytes(), &body)
data, _ := body["data"].(map[string]interface{})
warn, _ := data["use_warn"].(string)
if data["use_offered"] != true || data["use_future"] != true || !strings.Contains(warn, want) {
t.Fatalf("%s: a future-dated only copy is offered, marked and said: offered=%v future=%v warn=%q", lang, data["use_offered"], data["use_future"], warn)
}
}
}
+65 -13
View File
@@ -44,6 +44,9 @@ type KeptCopy struct {
// SnapshotID and UnitPath name the off-site copy (Tier 3 only): the snapshot and the unit's path in it. // SnapshotID and UnitPath name the off-site copy (Tier 3 only): the snapshot and the unit's path in it.
SnapshotID string SnapshotID string
UnitPath string UnitPath string
// Future (R-1271): the copy's date lies past this box's clock (+ KeptClockSkew) — chosen only when it is the only
// copy, and the question then says so and does not recommend it.
Future bool
} }
// KeptTierOffsite is KeptCopy.Tier for the off-site copy. // KeptTierOffsite is KeptCopy.Tier for the off-site copy.
@@ -112,11 +115,10 @@ func (m *Manager) KeptDBCopy(app, drive string) (KeptCopy, bool) {
if app == "" || m.isStackDeployed(app) { if app == "" || m.isStackDeployed(app) {
return KeptCopy{}, false return KeptCopy{}, false
} }
var best KeptCopy var cands []KeptCopy
found := false
consider := func(c KeptCopy, ok bool) { consider := func(c KeptCopy, ok bool) {
if ok && (!found || c.Time.After(best.Time)) { if ok {
best, found = c, true cands = append(cands, c)
} }
} }
if u, ok := m.RemovedAppUnitFor(app); ok { if u, ok := m.RemovedAppUnitFor(app); ok {
@@ -127,7 +129,7 @@ func (m *Manager) KeptDBCopy(app, drive string) (KeptCopy, bool) {
for _, d := range m.Tier2MirrorDirsForApp(app) { for _, d := range m.Tier2MirrorDirsForApp(app) {
consider(m.KeptCopyAt(tier2UnitDir(d), drive, 2)) consider(m.KeptCopyAt(tier2UnitDir(d), drive, 2))
} }
return best, found return keptPick(time.Now(), cands...) // R-1271: the newest TRUE date; own unit before the second drive on a tie
} }
// KeptOffsiteCopies is the off-site copy kept files of each app could be loaded from: the app's newest // KeptOffsiteCopies is the off-site copy kept files of each app could be loaded from: the app's newest
@@ -174,21 +176,71 @@ func (m *Manager) KeptOffsiteCopy(ctx context.Context, app string) (KeptCopy, bo
return c, ok return c, ok
} }
// KeptNewer chooses between a local copy and the off-site copy: the off-site one only when it is NEWER or // KeptNewerAt chooses between a local copy and the off-site copy at now: the off-site one only when it is NEWER by a
// the only one (a tie goes local — no download). // true date, or the only one (a tie goes local — no download). R-1271: a future-dated copy never counts as newer.
func KeptNewer(local KeptCopy, lok bool, off KeptCopy, ook bool) (KeptCopy, bool) { func KeptNewerAt(now time.Time, local KeptCopy, lok bool, off KeptCopy, ook bool) (KeptCopy, bool) {
if ook && (!lok || off.Time.After(local.Time)) { var cands []KeptCopy
return off, true if lok {
cands = append(cands, local)
} }
return local, lok if ook {
cands = append(cands, off)
}
return keptPick(now, cands...)
} }
// KeptBestCopy is the copy „Use my kept data" and Load use for app's kept files on drive: the newest // KeptBestCopy is the copy „Use my kept data" and Load use for app's kept files on drive: the newest
// usable local copy (KeptDBCopy), or the off-site copy when it is newer or the only one. // usable local copy (KeptDBCopy), or the off-site copy when it is newer by a true date or the only one.
func (m *Manager) KeptBestCopy(ctx context.Context, app, drive string) (KeptCopy, bool) { func (m *Manager) KeptBestCopy(ctx context.Context, app, drive string) (KeptCopy, bool) {
local, lok := m.KeptDBCopy(app, drive) local, lok := m.KeptDBCopy(app, drive)
off, ook := m.KeptOffsiteCopy(ctx, app) off, ook := m.KeptOffsiteCopy(ctx, app)
return KeptNewer(local, lok, off, ook) return KeptNewerAt(time.Now(), local, lok, off, ook)
}
// ── R-1271 (brief 2026-10-25 Part 4): a copy dated in the future is never „the newest" ────────────────────────────
//
// MEASURED (catalog sweep 2026-10-24, Tester4: Paperless-ngx, Nextcloud, Immich): the off-site store held copies of the
// 2026-10-20 rehearsal box, whose clock had been set to 2026-10-28. The off-site copy's date is the snapshot time that
// OTHER box wrote — this box recorded no data time to cap it (offsiteDataTime) — so „newer than every local copy" took
// it over the family's own copy of the same evening, and „Use my kept data" loaded it. The `07` §6.5 „taken of another
// drive" check (KeptCopyAt) compares the unit's HDD_PATH, a mount PATH with the same name on every box: a copy of
// another box's /mnt/felhom-drives/hdd_1 matches this box's. It is a path check, not a physical identity, and it stays
// one — loading the old box's data on a new box is what off-site exists for.
//
// THE RULE. A copy dated more than KeptClockSkew past now is FUTURE-DATED and never counts as the newest: the newest
// copy with a true date wins (a tie goes to the earlier candidate — local before off-site, own unit before the second
// drive). A future-dated copy is chosen only when no other exists; it is then marked Future, and the question says its
// date is in the future and does not recommend it. Pinned by kept_pick_test.go.
// KeptClockSkew is how far a copy's date may lie past this box's clock and still count as a true date: NTP keeps a box
// within a second; ten minutes covers a box whose clock synced late after a power cut.
const KeptClockSkew = 10 * time.Minute
// KeptFuture reports whether c's date lies more than KeptClockSkew past now.
func KeptFuture(c KeptCopy, now time.Time) bool { return c.Time.After(now.Add(KeptClockSkew)) }
// keptPick: the newest candidate with a true date (the first one on a tie); only when every candidate is
// future-dated, the first one, marked Future.
func keptPick(now time.Time, cands ...KeptCopy) (KeptCopy, bool) {
var best KeptCopy
found := false
for _, c := range cands {
if KeptFuture(c, now) {
continue
}
if !found || c.Time.After(best.Time) {
best, found = c, true
}
}
if found {
return best, true
}
if len(cands) == 0 {
return KeptCopy{}, false
}
c := cands[0]
c.Future = true
return c, true
} }
// Refusals of an off-site load, born as bundle keys. Each is raised BEFORE prepare and before anything // Refusals of an off-site load, born as bundle keys. Each is raised BEFORE prepare and before anything
@@ -0,0 +1,66 @@
package backup
import (
"testing"
"time"
)
// R-1271 (brief 2026-10-25 Part 4) — a copy dated in the future is never „the newest". The measured case (catalog sweep
// 2026-10-24): the rehearsal box's off-site copy dated 2026-10-28 07:39 beat the family's own copy of 2026-10-24 19:17
// on a box whose clock said 2026-10-24.
// COMPANION RED-PROOF (REPORT.md): with the old KeptNewer (off-site when its date is after the local one's),
// TestR1271_AFutureOffsiteCopyNeverBeatsTheOwnCopy fails — the off-site copy is chosen.
var r1271Now = time.Date(2026, 10, 24, 19, 22, 0, 0, time.UTC)
func TestR1271_AFutureOffsiteCopyNeverBeatsTheOwnCopy(t *testing.T) {
local := KeptCopy{UnitDir: "/u", Tier: 1, Time: time.Date(2026, 10, 24, 19, 17, 0, 0, time.UTC)}
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 28, 6, 39, 0, 0, time.UTC), SnapshotID: "abba81ba"}
c, ok := KeptNewerAt(r1271Now, local, true, off, true)
if !ok || c.Tier != 1 || c.Future {
t.Fatalf("the household's own copy must win over a future-dated off-site one, got %+v", c)
}
}
func TestR1271_AFutureCopyAloneIsOfferedButMarked(t *testing.T) {
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 28, 6, 39, 0, 0, time.UTC), SnapshotID: "abba81ba"}
c, ok := KeptNewerAt(r1271Now, KeptCopy{}, false, off, true)
if !ok || c.Tier != KeptTierOffsite || !c.Future {
t.Fatalf("a future-dated copy alone is offered and marked Future (not recommended), got %+v ok=%v", c, ok)
}
}
func TestR1271_AnOlderOffsiteCopyAloneIsChosenAsBefore(t *testing.T) {
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 20, 4, 15, 0, 0, time.UTC), SnapshotID: "s1"}
c, ok := KeptNewerAt(r1271Now, KeptCopy{}, false, off, true)
if !ok || c.Tier != KeptTierOffsite || c.Future {
t.Fatalf("a normal older off-site copy alone is chosen as before, got %+v", c)
}
}
// The other direction of the old rule still holds: a NEWER off-site copy with a true date beats an older local one.
func TestR1271_ANewerTrueOffsiteCopyStillWins(t *testing.T) {
local := KeptCopy{UnitDir: "/u", Tier: 1, Time: time.Date(2026, 10, 20, 4, 0, 0, 0, time.UTC)}
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 24, 4, 0, 0, 0, time.UTC), SnapshotID: "s2"}
if c, _ := KeptNewerAt(r1271Now, local, true, off, true); c.Tier != KeptTierOffsite {
t.Fatalf("a newer off-site copy with a true date wins, got %+v", c)
}
// A tie goes local (no download).
off.Time = local.Time
if c, _ := KeptNewerAt(r1271Now, local, true, off, true); c.Tier != 1 {
t.Fatalf("a tie goes local, got %+v", c)
}
}
// The skew allowance: a few minutes ahead is a true date; beyond KeptClockSkew it is not.
func TestR1271_TheClockSkewAllowance(t *testing.T) {
local := KeptCopy{UnitDir: "/u", Tier: 1, Time: r1271Now.Add(-time.Hour)}
near := KeptCopy{Tier: KeptTierOffsite, Time: r1271Now.Add(KeptClockSkew - time.Minute), SnapshotID: "n"}
if c, _ := KeptNewerAt(r1271Now, local, true, near, true); c.Tier != KeptTierOffsite {
t.Fatalf("a copy within the skew is a true date and newer, got %+v", c)
}
far := KeptCopy{Tier: KeptTierOffsite, Time: r1271Now.Add(KeptClockSkew + time.Minute), SnapshotID: "f"}
if c, _ := KeptNewerAt(r1271Now, local, true, far, true); c.Tier != 1 {
t.Fatalf("a copy beyond the skew is future-dated and never the newest, got %+v", c)
}
}
+3 -1
View File
@@ -2864,5 +2864,7 @@
"err.backup.restore_not_started": "%s: we loaded the data back, but the app has not started yet. Wait a few minutes, then open it again. If it stays like this, tell us.", "err.backup.restore_not_started": "%s: we loaded the data back, but the app has not started yet. Wait a few minutes, then open it again. If it stays like this, tell us.",
"note.restore.late_started": "%s has started: the restore is done.", "note.restore.late_started": "%s has started: the restore is done.",
"err.backup.restore_image_gone": "The version of this backup (%s) cannot be downloaded: its maker no longer offers it. We did not touch the app. Choose another backup.", "err.backup.restore_image_gone": "The version of this backup (%s) cannot be downloaded: its maker no longer offers it. We did not touch the app. Choose another backup.",
"err.backup.restore_image_unreachable": "The version of this backup (%s) cannot be downloaded right now, so we did not touch the app. Try again later, or choose another backup." "err.backup.restore_image_unreachable": "The version of this backup (%s) cannot be downloaded right now, so we did not touch the app. Try again later, or choose another backup.",
"kept.backup.future_note": "(its date is in the future — the clock of the box that made it may have been wrong)",
"kept.choice.use.future": "Note: the date of this backup (%s) is in the future. The clock of the box that made it may have been wrong. Choose it only if you are sure it is your backup."
} }
+3 -1
View File
@@ -2852,5 +2852,7 @@
"err.backup.restore_not_started": "A(z) %s: az adatokat visszatöltöttük, de az alkalmazás még nem indult el. Várj pár percet, majd nyisd meg újra. Ha így marad, szólj nekünk.", "err.backup.restore_not_started": "A(z) %s: az adatokat visszatöltöttük, de az alkalmazás még nem indult el. Várj pár percet, majd nyisd meg újra. Ha így marad, szólj nekünk.",
"note.restore.late_started": "A(z) %s elindult: a visszaállítás kész.", "note.restore.late_started": "A(z) %s elindult: a visszaállítás kész.",
"err.backup.restore_image_gone": "Ennek a mentésnek a verzióját (%s) nem lehet letölteni: a készítője már nem kínálja. Az alkalmazáshoz nem nyúltunk. Válassz egy másik mentést.", "err.backup.restore_image_gone": "Ennek a mentésnek a verzióját (%s) nem lehet letölteni: a készítője már nem kínálja. Az alkalmazáshoz nem nyúltunk. Válassz egy másik mentést.",
"err.backup.restore_image_unreachable": "Ennek a mentésnek a verzióját (%s) most nem lehet letölteni, ezért az alkalmazáshoz nem nyúltunk. Próbáld újra később, vagy válassz egy másik mentést." "err.backup.restore_image_unreachable": "Ennek a mentésnek a verzióját (%s) most nem lehet letölteni, ezért az alkalmazáshoz nem nyúltunk. Próbáld újra később, vagy válassz egy másik mentést.",
"kept.backup.future_note": "(a dátuma a jövőben van — a mentést készítő doboz órája rosszul járhatott)",
"kept.choice.use.future": "Figyelem: ennek a mentésnek a dátuma (%s) a jövőben van. A mentést készítő doboz órája rosszul járhatott. Csak akkor válaszd, ha biztosan ez a ti mentésetek."
} }
+12 -3
View File
@@ -80,18 +80,27 @@ func (s *Server) keptBackupFor(lang string, it stacks.KeptItem, off map[string]b
// A dated folder's own unit is the copy taken with those files. // A dated folder's own unit is the copy taken with those files.
if it.UnitDir != "" { if it.UnitDir != "" {
if c, ok := s.backupMgr.KeptCopyAt(it.UnitDir, it.Drive, 1); ok { if c, ok := s.backupMgr.KeptCopyAt(it.UnitDir, it.Drive, 1); ok {
return s.msgLang(lang, "kept.backup.with", fmtLocalTime(lang, c.Time)), c, true name := s.msgLang(lang, "kept.backup.with", fmtLocalTime(lang, c.Time))
if backup.KeptFuture(c, time.Now()) { // R-1271: say it, never hide it
c.Future = true
name += " " + s.msgLang(lang, "kept.backup.future_note")
}
return name, c, true
} }
} }
} else { } else {
local, lok = s.backupMgr.KeptDBCopy(it.App, it.Drive) local, lok = s.backupMgr.KeptDBCopy(it.App, it.Drive)
} }
oc, ook := off[it.App] oc, ook := off[it.App]
c, ok := backup.KeptNewer(local, lok, oc, ook && it.App != "") c, ok := backup.KeptNewerAt(time.Now(), local, lok, oc, ook && it.App != "")
if !ok { if !ok {
return none, backup.KeptCopy{}, false return none, backup.KeptCopy{}, false
} }
return s.msgLang(lang, backup.KeptCopyKey(c.Tier), fmtLocalTime(lang, c.Time)), c, true name := s.msgLang(lang, backup.KeptCopyKey(c.Tier), fmtLocalTime(lang, c.Time))
if c.Future { // R-1271: the only copy, dated in the future — offered, and said
name += " " + s.msgLang(lang, "kept.backup.future_note")
}
return name, c, true
} }
// keptOffsite asks the off-site repository ONCE for every listed app. // keptOffsite asks the off-site repository ONCE for every listed app.
@@ -147,9 +147,9 @@ function showKeptChoice(d, form) {
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; } function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title)); box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body)); box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) { function option(label, desc, choice, enabled, extra, plain) {
var o = el('div', 'kept-choice-option'); var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label); var b = el('button', enabled && !plain ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button'; b.type = 'button';
b.disabled = !enabled; b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice); b.setAttribute('data-kept-choice', choice);
@@ -159,7 +159,7 @@ function showKeptChoice(d, form) {
if (extra) o.appendChild(el('p', 'kept-choice-note', extra)); if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o; return o;
} }
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, '')); box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, d.use_warn || '', !!d.use_future));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up)); box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false; box.hidden = false;
box.scrollIntoView({behavior: 'smooth'}); box.scrollIntoView({behavior: 'smooth'});
@@ -501,9 +501,9 @@ function showKeptChoice(d, form) {
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; } function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title)); box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body)); box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) { function option(label, desc, choice, enabled, extra, plain) {
var o = el('div', 'kept-choice-option'); var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label); var b = el('button', enabled && !plain ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button'; b.type = 'button';
b.disabled = !enabled; b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice); b.setAttribute('data-kept-choice', choice);
@@ -513,7 +513,7 @@ function showKeptChoice(d, form) {
if (extra) o.appendChild(el('p', 'kept-choice-note', extra)); if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o; return o;
} }
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, '')); box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, d.use_warn || '', !!d.use_future));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up)); box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false; box.hidden = false;
box.scrollIntoView({behavior: 'smooth'}); box.scrollIntoView({behavior: 'smooth'});
@@ -457,9 +457,9 @@ function showKeptChoice(d, form) {
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; } function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title)); box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body)); box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) { function option(label, desc, choice, enabled, extra, plain) {
var o = el('div', 'kept-choice-option'); var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label); var b = el('button', enabled && !plain ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button'; b.type = 'button';
b.disabled = !enabled; b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice); b.setAttribute('data-kept-choice', choice);
@@ -469,7 +469,7 @@ function showKeptChoice(d, form) {
if (extra) o.appendChild(el('p', 'kept-choice-note', extra)); if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o; return o;
} }
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, '')); box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, d.use_warn || '', !!d.use_future));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up)); box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false; box.hidden = false;
box.scrollIntoView({behavior: 'smooth'}); box.scrollIntoView({behavior: 'smooth'});
@@ -479,9 +479,9 @@ function showKeptChoice(d, form) {
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; } function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title)); box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body)); box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) { function option(label, desc, choice, enabled, extra, plain) {
var o = el('div', 'kept-choice-option'); var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label); var b = el('button', enabled && !plain ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button'; b.type = 'button';
b.disabled = !enabled; b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice); b.setAttribute('data-kept-choice', choice);
@@ -491,7 +491,7 @@ function showKeptChoice(d, form) {
if (extra) o.appendChild(el('p', 'kept-choice-note', extra)); if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o; return o;
} }
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, '')); box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, d.use_warn || '', !!d.use_future));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up)); box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false; box.hidden = false;
box.scrollIntoView({behavior: 'smooth'}); box.scrollIntoView({behavior: 'smooth'});
+3 -1
View File
@@ -326,7 +326,9 @@
"err.backup.restore_not_started": "BORN AS A KEY (R-1270, brief 2026-10-25 Part 3) -- a restore whose data is back but whose app did not start in time is never a success; its sentence, and the late start that turns it into one. Pinned by internal/backup/restore_verdict_test.go TestR1270_*.", "err.backup.restore_not_started": "BORN AS A KEY (R-1270, brief 2026-10-25 Part 3) -- a restore whose data is back but whose app did not start in time is never a success; its sentence, and the late start that turns it into one. Pinned by internal/backup/restore_verdict_test.go TestR1270_*.",
"note.restore.late_started": "BORN AS A KEY (R-1270, brief 2026-10-25 Part 3) -- a restore whose data is back but whose app did not start in time is never a success; its sentence, and the late start that turns it into one. Pinned by internal/backup/restore_verdict_test.go TestR1270_*.", "note.restore.late_started": "BORN AS A KEY (R-1270, brief 2026-10-25 Part 3) -- a restore whose data is back but whose app did not start in time is never a success; its sentence, and the late start that turns it into one. Pinned by internal/backup/restore_verdict_test.go TestR1270_*.",
"err.backup.restore_image_gone": "BORN AS A KEY (D48, operator ruling 2026-10-25; 09 decision 257) -- a restore downloads first: the refusal when an image of the backup's version cannot be downloaded (deleted by its maker, or the registry busy or unreachable); the app untouched. Pinned by internal/backup/restore_pull_test.go TestD48_*.", "err.backup.restore_image_gone": "BORN AS A KEY (D48, operator ruling 2026-10-25; 09 decision 257) -- a restore downloads first: the refusal when an image of the backup's version cannot be downloaded (deleted by its maker, or the registry busy or unreachable); the app untouched. Pinned by internal/backup/restore_pull_test.go TestD48_*.",
"err.backup.restore_image_unreachable": "BORN AS A KEY (D48, operator ruling 2026-10-25; 09 decision 257) -- a restore downloads first: the refusal when an image of the backup's version cannot be downloaded (deleted by its maker, or the registry busy or unreachable); the app untouched. Pinned by internal/backup/restore_pull_test.go TestD48_*." "err.backup.restore_image_unreachable": "BORN AS A KEY (D48, operator ruling 2026-10-25; 09 decision 257) -- a restore downloads first: the refusal when an image of the backup's version cannot be downloaded (deleted by its maker, or the registry busy or unreachable); the app untouched. Pinned by internal/backup/restore_pull_test.go TestD48_*.",
"kept.backup.future_note": "BORN AS A KEY (R-1271, brief 2026-10-25 Part 4) -- a copy dated in the future is never the newest; when it is the only copy the kept-data question and the kept list say its date is in the future. Pinned by internal/backup/kept_pick_test.go and internal/api/r1271_future_copy_test.go.",
"kept.choice.use.future": "BORN AS A KEY (R-1271, brief 2026-10-25 Part 4) -- a copy dated in the future is never the newest; when it is the only copy the kept-data question and the kept list say its date is in the future. Pinned by internal/backup/kept_pick_test.go and internal/api/r1271_future_copy_test.go."
}, },
"flash.share.already_on": "A megosztás már be van kapcsolva.", "flash.share.already_on": "A megosztás már be van kapcsolva.",
"flash.share.enable_failed": "A megosztás bekapcsolása nem sikerült.", "flash.share.enable_failed": "A megosztás bekapcsolása nem sikerült.",