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
gates / gates (push) Successful in 1m33s
This commit is contained in:
@@ -20,6 +20,10 @@ type keptChoiceData struct {
|
||||
UseDesc string `json:"use_desc"` // "" when use is off
|
||||
UseOff string `json:"use_off"` // "" when use is offered
|
||||
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"`
|
||||
FreshDesc string `json:"fresh_desc"`
|
||||
NotBacked string `json:"not_backed_up"`
|
||||
@@ -71,13 +75,17 @@ func (r *Router) keptDataAtInstall(w http.ResponseWriter, req *http.Request, nam
|
||||
}
|
||||
if cp.ok {
|
||||
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 {
|
||||
data.UseOff = r.msgLang(lang, "kept.choice.use_off")
|
||||
}
|
||||
|
||||
switch choice {
|
||||
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})
|
||||
return true
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 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.
|
||||
@@ -112,11 +115,10 @@ func (m *Manager) KeptDBCopy(app, drive string) (KeptCopy, bool) {
|
||||
if app == "" || m.isStackDeployed(app) {
|
||||
return KeptCopy{}, false
|
||||
}
|
||||
var best KeptCopy
|
||||
found := false
|
||||
var cands []KeptCopy
|
||||
consider := func(c KeptCopy, ok bool) {
|
||||
if ok && (!found || c.Time.After(best.Time)) {
|
||||
best, found = c, true
|
||||
if ok {
|
||||
cands = append(cands, c)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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
|
||||
@@ -174,21 +176,71 @@ func (m *Manager) KeptOffsiteCopy(ctx context.Context, app string) (KeptCopy, bo
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// KeptNewer chooses between a local copy and the off-site copy: the off-site one only when it is NEWER or
|
||||
// the only one (a tie goes local — no download).
|
||||
func KeptNewer(local KeptCopy, lok bool, off KeptCopy, ook bool) (KeptCopy, bool) {
|
||||
if ook && (!lok || off.Time.After(local.Time)) {
|
||||
return off, true
|
||||
// KeptNewerAt chooses between a local copy and the off-site copy at now: the off-site one only when it is NEWER by a
|
||||
// true date, or the only one (a tie goes local — no download). R-1271: a future-dated copy never counts as newer.
|
||||
func KeptNewerAt(now time.Time, local KeptCopy, lok bool, off KeptCopy, ook bool) (KeptCopy, bool) {
|
||||
var cands []KeptCopy
|
||||
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
|
||||
// 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) {
|
||||
local, lok := m.KeptDBCopy(app, drive)
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"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_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."
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
"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_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."
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
if it.UnitDir != "" {
|
||||
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 {
|
||||
local, lok = s.backupMgr.KeptDBCopy(it.App, it.Drive)
|
||||
}
|
||||
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 {
|
||||
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.
|
||||
|
||||
@@ -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; }
|
||||
box.appendChild(el('h3', 'kept-choice-title', d.title));
|
||||
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 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.disabled = !enabled;
|
||||
b.setAttribute('data-kept-choice', choice);
|
||||
@@ -159,7 +159,7 @@ function showKeptChoice(d, form) {
|
||||
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
|
||||
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.hidden = false;
|
||||
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; }
|
||||
box.appendChild(el('h3', 'kept-choice-title', d.title));
|
||||
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 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.disabled = !enabled;
|
||||
b.setAttribute('data-kept-choice', choice);
|
||||
@@ -513,7 +513,7 @@ function showKeptChoice(d, form) {
|
||||
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
|
||||
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.hidden = false;
|
||||
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; }
|
||||
box.appendChild(el('h3', 'kept-choice-title', d.title));
|
||||
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 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.disabled = !enabled;
|
||||
b.setAttribute('data-kept-choice', choice);
|
||||
@@ -469,7 +469,7 @@ function showKeptChoice(d, form) {
|
||||
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
|
||||
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.hidden = false;
|
||||
box.scrollIntoView({behavior: 'smooth'});
|
||||
|
||||
+3
-3
@@ -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; }
|
||||
box.appendChild(el('h3', 'kept-choice-title', d.title));
|
||||
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 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.disabled = !enabled;
|
||||
b.setAttribute('data-kept-choice', choice);
|
||||
@@ -491,7 +491,7 @@ function showKeptChoice(d, form) {
|
||||
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
|
||||
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.hidden = false;
|
||||
box.scrollIntoView({behavior: 'smooth'});
|
||||
|
||||
@@ -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_*.",
|
||||
"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_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.enable_failed": "A megosztás bekapcsolása nem sikerült.",
|
||||
|
||||
Reference in New Issue
Block a user