D47 follow-up (found live on 9202): the kept-copies list („Megőrzött másolatok”) shows on a box without off-site too — it sat only inside the off-site card, so 9202 never listed a set-aside unit or a D25 copy; one named template, two call sites, parity fixtures unchanged. Red-proved. Commit only.
gates / gates (push) Successful in 1m26s

This commit is contained in:
2026-10-25 10:01:56 +01:00
parent bd9f4f7b74
commit 1f9c863f79
4 changed files with 77 additions and 40 deletions
+4
View File
@@ -109,6 +109,10 @@ unchanged. Design: felhom.eu `07` §6.3 / §8.2a.
/backup/kept/restore`, by its record only — path inside its tree, same device + inode) beside „Eltávolítás". Remove /backup/kept/restore`, by its record only — path inside its tree, same device + inode) beside „Eltávolítás". Remove
with „delete its backups too" deletes the app's set-aside units too (the D25 / R-1252 H9 rule). with „delete its backups too" deletes the app's set-aside units too (the D25 / R-1252 H9 rule).
- A put-back writes the live install's identity back (the live definition copy carries it). - A put-back writes the live install's identity back (the live definition copy carries it).
- **Found live on 9202, fixed without a row: the „Megőrzött másolatok" list was inside the off-site card** of the
restore page, so a box without off-site never listed a kept copy (D25's second-drive copies included). The section is
one named template now, shown in the off-site card when the box has off-site and after it when not
(`TestD47_KeptCopiesAreListedWithoutOffsite`, seen failing; the parity fixtures are unchanged).
- Tests (each seen failing with the guard off — `audits/restore-truth-2026-10-25/redproof/`): - Tests (each seen failing with the guard off — `audits/restore-truth-2026-10-25/redproof/`):
`TestD47_TheSweepOrderNeverRewritesTheKeptUnit`, `…AUnitFromBeforeD47IsSetAsideWhenItsPasswordDiffers`, `TestD47_TheSweepOrderNeverRewritesTheKeptUnit`, `…AUnitFromBeforeD47IsSetAsideWhenItsPasswordDiffers`,
`…TheSetAsideUnitRestoresWithItsOwnPassword`, `…TheWriterRefusesWhenTheUnitCannotBeSetAside`, `…TheSetAsideUnitRestoresWithItsOwnPassword`, `…TheWriterRefusesWhenTheUnitCannotBeSetAside`,
+10
View File
@@ -1,5 +1,15 @@
# CONTEXT.md — Project Memory # CONTEXT.md — Project Memory
> **2026-10-25 (Sunday, unattended) — D47, D48, R-1270, R-1271 on `main`, NOT released (next v0.343.0, MinAgent 0.158.1
> unchanged).** D47 (`02daebe`): `stacks.AppConfig.InstallID` (minted at install/import, taken from the unit by a
> restore, `EnsureInstallID` for older apps); `backup/unit_owner.go` `guardUnitWrite`/`guardUnitDir` in every unit writer
> (capture, refresh, dumps, update pre-backup, volume dump, second-drive unit copy, stale-drive prune) sets another
> install's unit aside whole (`backups/set-aside/<app>/<stamp>`, a kept copy kind `unit`, restore page „Visszaállítás").
> R-1270 (`b680db8`): `ErrRestoreNotStarted` from every restore path on a health timeout; undo copy kept; late-start
> watcher. D48 (`2d71270`): `prepareRestoreImages` before the stop on every restore path under
> `dockerexec.HoldImageWork`. R-1271 (`bd9f4f7`): `keptPick`/`KeptNewerAt` — a copy more than 10 min in the future is
> never the newest. Live proofs on 9202 with `0.343.0-rc.1`: felhom.eu `audits/restore-truth-2026-10-25/`.
> **2026-10-24 night (unattended restore-holes run) — on `main`, NOT released (next v0.343.0, MinAgent 0.158.1 unchanged).** > **2026-10-24 night (unattended restore-holes run) — on `main`, NOT released (next v0.343.0, MinAgent 0.158.1 unchanged).**
> R-1251: a pending put-back blocks every start (`backup/appstop_putback_wait.go`: restore hold `putback_pending`, the > R-1251: a pending put-back blocks every start (`backup/appstop_putback_wait.go`: restore hold `putback_pending`, the
> hold store every start path honours; `ResumePutBack` in the boot recreate and `restartStacks`; `ResumeWaitingPutBacks` > hold store every start path honours; `ResumePutBack` in the boot recreate and `restartStacks`; `ResumeWaitingPutBacks`
@@ -163,3 +163,22 @@ func TestD47_KeptUnitRestoreHandlerRefusals(t *testing.T) {
t.Fatal("a refusal must start nothing") t.Fatal("a refusal must start nothing")
} }
} }
// D25 + D47 (2026-10-25, found live on 9202): a box WITHOUT off-site lists its kept copies too. Before, the section sat
// only inside the off-site card, so 9202 (no off-site) never showed a set-aside unit or a kept second-drive copy.
func TestD47_KeptCopiesAreListedWithoutOffsite(t *testing.T) {
d := i18nLayoutData("backups-restore", "Visszaállítás a mentésből")
d["Backup"] = i18nBackupStatus(false, false)
d["OffboxConfigured"] = false
d["KeptCopies"] = []keptCopyRow{{ID: "aaaaaaaaaaaaaaaa", Source: "tandoor", Line: "belso", SizeHuman: "150 MB", Since: "2026-10-25T08:00:00Z", Restorable: true}}
d["KeptUnitRestorable"] = true
html := renderBackupPage(t, "backups_restore", d)
if strings.Count(html, "Megőrzött másolatok") != 1 || strings.Count(html, `action="/backup/kept/restore"`) != 1 {
t.Fatal("a box without off-site must list its kept copies once, with the restore")
}
d["OffboxConfigured"] = true
d["OffsiteStoreState"] = "known"
if html := renderBackupPage(t, "backups_restore", d); strings.Count(html, "Megőrzött másolatok") != 1 {
t.Fatal("with off-site the section is shown once (inside the off-site card), never twice")
}
}
@@ -167,45 +167,7 @@
<!-- v0.147.0 (4a): what the verification restores actually LEFT on disk. Until now the result of <!-- v0.147.0 (4a): what the verification restores actually LEFT on disk. Until now the result of
an ellenőrző visszaállítás was invisible — the flash said "a verification folder on the an ellenőrző visszaállítás was invisible — the flash said "a verification folder on the
drive" without naming it, and nothing listed what had accumulated. Full path, size and date, drive" without naming it, and nothing listed what had accumulated. Full path, size and date,
so the copy can be found, opened, and cleaned up. -->{{/* D25 (operator ruling 2026-10-21; R-1175, R-1181): second-drive copies whose source is gone (a drive away or removed, a folder missing). No night job removes them; only „Eltávolítás" here, after a confirm. On one line with the comment above so a page without kept copies renders byte-identically. */}}{{if .KeptCopies}} so the copy can be found, opened, and cleaned up. -->{{/* D25 (operator ruling 2026-10-21; R-1175, R-1181): second-drive copies whose source is gone (a drive away or removed, a folder missing). No night job removes them; only „Eltávolítás" here, after a confirm. On one line with the comment above so a page without kept copies renders byte-identically. */}}{{template "kept_copies_section" .}}
<div class="backup-tier-divider" style="margin-top:1.5rem"></div>
<h3 style="margin-bottom:.5rem">{{T "backups_restore.megorzott_masolatok"}}</h3>
<p class="form-hint" style="margin-bottom:.75rem">{{T "backups_restore.megorzott_masolatok_hint"}}</p>
<div class="app-row-list">
{{range .KeptCopies}}
{{template "app_list_row" dict "Slug" "" "Name" .Source "Secondary" (printf "%s · %s · %s" .Line (numText .SizeHuman) (fmtTimeStr .Since))}}{{if .Restorable}}
<form method="POST" action="/backup/kept/restore" style="display:inline">
{{$.CSRFField}}
<input type="hidden" name="id" value="{{.ID}}">
<input type="hidden" name="confirm" value="1">
<button type="button" class="btn btn-xs btn-outline"
onclick="confirmRestoreKeptUnit(this)">{{T "backups_restore.visszaallitas"}}</button>
</form>{{end}}
<form method="POST" action="/backup/kept/remove" style="display:inline">
{{$.CSRFField}}
<input type="hidden" name="id" value="{{.ID}}">
<input type="hidden" name="confirm" value="1">
<button type="button" class="btn btn-xs btn-danger-outline"
onclick="confirmRemoveKeptCopy(this)">{{T "backups_restore.eltavolitas"}}</button>
</form>
{{template "app_list_row_end"}}
{{end}}
</div>
<script>
{{if .KeptUnitRestorable}}function confirmRestoreKeptUnit(btn){
felhomConfirm(btn, '{{T "backups_restore.megorzott_visszaallitas_megerosites"}}', function(){
var f = btn.closest('form');
if (f) { if (f.requestSubmit) f.requestSubmit(); else f.submit(); }
});
}
{{end}}function confirmRemoveKeptCopy(btn){
felhomConfirm(btn, '{{T "backups_restore.megorzott_eltavolitas_megerosites"}}', function(){
var f = btn.closest('form');
if (f) { if (f.requestSubmit) f.requestSubmit(); else f.submit(); }
});
}
</script>
{{end}}
<div class="backup-tier-divider" style="margin-top:1.5rem"></div> <div class="backup-tier-divider" style="margin-top:1.5rem"></div>
<h3 style="margin-bottom:.5rem">{{T "backups_restore.meglevo_ellenorzo_masolatok"}}</h3> <h3 style="margin-bottom:.5rem">{{T "backups_restore.meglevo_ellenorzo_masolatok"}}</h3>
{{if .OffsiteRestoreCopies}} {{if .OffsiteRestoreCopies}}
@@ -228,7 +190,7 @@
<p class="form-hint">{{T "backups_restore.nincs_ellenorzo_masolat_a_meghajton"}}</p> <p class="form-hint">{{T "backups_restore.nincs_ellenorzo_masolat_a_meghajton"}}</p>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}{{if not .OffboxConfigured}}{{template "kept_copies_section" .}}{{end}}
<!-- Hordozható mentéscsomag (.fab) — v0.124.0, decision 3: PORTABILITY, not a backup tier (no <!-- Hordozható mentéscsomag (.fab) — v0.124.0, decision 3: PORTABILITY, not a backup tier (no
@@ -464,3 +426,45 @@ function submitRestore() {
{{template "layout_end" .}} {{template "layout_end" .}}
{{end}} {{end}}
{{/* D25 + D47: the kept copies (second-drive copies whose source is gone; set-aside units of an earlier install). Shown
inside the off-site card when the box has off-site, and after it when it has none — before 2026-10-25 it sat only inside
the off-site block, so a box without off-site never listed its kept copies. One body, two call sites. */}}
{{define "kept_copies_section"}}{{if .KeptCopies}}
<div class="backup-tier-divider" style="margin-top:1.5rem"></div>
<h3 style="margin-bottom:.5rem">{{T "backups_restore.megorzott_masolatok"}}</h3>
<p class="form-hint" style="margin-bottom:.75rem">{{T "backups_restore.megorzott_masolatok_hint"}}</p>
<div class="app-row-list">
{{range .KeptCopies}}
{{template "app_list_row" dict "Slug" "" "Name" .Source "Secondary" (printf "%s · %s · %s" .Line (numText .SizeHuman) (fmtTimeStr .Since))}}{{if .Restorable}}
<form method="POST" action="/backup/kept/restore" style="display:inline">
{{$.CSRFField}}
<input type="hidden" name="id" value="{{.ID}}">
<input type="hidden" name="confirm" value="1">
<button type="button" class="btn btn-xs btn-outline"
onclick="confirmRestoreKeptUnit(this)">{{T "backups_restore.visszaallitas"}}</button>
</form>{{end}}
<form method="POST" action="/backup/kept/remove" style="display:inline">
{{$.CSRFField}}
<input type="hidden" name="id" value="{{.ID}}">
<input type="hidden" name="confirm" value="1">
<button type="button" class="btn btn-xs btn-danger-outline"
onclick="confirmRemoveKeptCopy(this)">{{T "backups_restore.eltavolitas"}}</button>
</form>
{{template "app_list_row_end"}}
{{end}}
</div>
<script>
{{if .KeptUnitRestorable}}function confirmRestoreKeptUnit(btn){
felhomConfirm(btn, '{{T "backups_restore.megorzott_visszaallitas_megerosites"}}', function(){
var f = btn.closest('form');
if (f) { if (f.requestSubmit) f.requestSubmit(); else f.submit(); }
});
}
{{end}}function confirmRemoveKeptCopy(btn){
felhomConfirm(btn, '{{T "backups_restore.megorzott_eltavolitas_megerosites"}}', function(){
var f = btn.closest('form');
if (f) { if (f.requestSubmit) f.requestSubmit(); else f.submit(); }
});
}
</script>
{{end}}{{end}}