controller v0.242.0: a removed app is listed with its kept backup; five small ones (R-487 R-491 R-490 R-489 R-476 R-456)
gates / gates (push) Successful in 14s

R-487: the local backup lists are keyed on the drives, not on what is
deployed — a removed app whose unit was kept is listed with the restore
that reinstalls it, the picker answers for it, and the restore opens the
unit where it sits. R-491: a removal clears the app's update hold.
R-490: /api/system/info reaches the API router and reads the default
storage path. R-489: volumes_removed is the real before/after difference,
[] when none. R-476: a Tier-2 copy is dated by its data, not its manifest.
R-456: the boot-orphan rule is pinned. Every fix red-proofed.
This commit is contained in:
2026-09-13 22:50:18 +02:00
parent c1f62ddae8
commit d698ce343b
22 changed files with 838 additions and 13 deletions
@@ -94,6 +94,12 @@ type Tier2Coverage struct {
// it is a fact about the artifact the restore will actually open. "" means UNKNOWN.
UnitPackageDate string
UnitLegPreserved bool
// UnitDataDate (R-476) — the newest ARTIFACT in the mirrored unit: its dumps' mtime, or the
// manifest's when nothing is newer. The manifest moves only when the app's DEFINITION changes
// (checksum-skip), while the nightly dumps keep their names and their fresh bytes — so on
// demo-hp a copy holding a dump written at 00:30Z was dated by a manifest from the day before.
// RFC3339 UTC; "" when the unit is not readable.
UnitDataDate string
}
// CanRestore reports whether the FILE restore has any subtree to read at all.
@@ -142,6 +148,9 @@ func tier2CoverageAt(destBase string) Tier2Coverage {
// R-403: ask the package itself when it was made. Reading the artifact rather than the status
// record is what makes this date impossible to overstate.
c.UnitPackageDate = unitPackageDate(unitDir)
if newest, ok := unitNewestArtifact(unitDir); ok {
c.UnitDataDate = newest.UTC().Format(time.RFC3339)
}
return c
}
@@ -299,11 +308,19 @@ func (c Tier2Coverage) Tier2CopyDate() (date string, proven bool) {
// `12:03:49Z` against a run at `12:14:24Z` — perfectly healthy, and all four would have been told
// their package was stale. A warning that fires on everything is a warning nobody reads, which costs
// the same as the comforting lie it was meant to replace.
//
// R-476: when the leg was NOT preserved, the package's date is its DATA time — the newest dump in
// the copy — never the manifest's, which moves only when the definition changes and so undersold a
// fresh copy by a day. A PRESERVED package keeps the manifest date: nothing in it is newer, and the
// R-403 rule that a preserved package is never shown as fresh is what this sits under.
func (c Tier2Coverage) UnitRestoreDate() (date string, preserved bool) {
if c.UnitPackageDate == "" {
copyDate, _ := c.Tier2CopyDate()
return copyDate, c.UnitLegPreserved
}
if !c.UnitLegPreserved && c.UnitDataDate != "" && c.UnitDataDate > c.UnitPackageDate {
return c.UnitDataDate, false
}
return c.UnitPackageDate, c.UnitLegPreserved
}