R-1278: the install page says when a backup of an earlier install is kept and links the restore page (HU and EN). Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
2026-10-25 23:32:01 +01:00
parent 7ea942a118
commit fbec5cb499
10 changed files with 1368 additions and 0 deletions
+11
View File
@@ -2,6 +2,15 @@
**MinAgent: 0.158.1** (unchanged). The hold acts only on a box whose agent reports `waiting_for_quiet` (≥ 0.165.0). **MinAgent: 0.158.1** (unchanged). The hold acts only on a box whose agent reports `waiting_for_quiet` (≥ 0.165.0).
### The install page says when a backup of an earlier install is kept (R-1278, P3)
- `internal/web/handlers.go` + `templates/deploy.html`: for an app not installed, when the box keeps a backup of an
earlier install (`backup.KeptBackupsFor`: its set-aside units, D47, and the unit „remove — keep backups" left), an
info notice says the new install starts empty and links „A megőrzött mentések megnyitása" (`/backups/restore`). HU and
EN keys `deploy.kept_backup_offer{,_link}`; the hidden case renders byte for byte as before (the parity fixtures
unchanged); a new parity case `deploy_new_kept_backup_r1278`. Tests: `TestR1278_*` (render per branch, the wiring, the
count). Red-proof: felhom.eu `documentation/audits/night-2026-10-25/redproof-r1278.txt`.
### FileBrowser is off the app network: only traefik (and a linked OnlyOffice) reach it (R-1281, P2, D44 #23) ### FileBrowser is off the app network: only traefik (and a linked OnlyOffice) reach it (R-1281, P2, D44 #23)
- **Measured before** (the night's rig, 0.344.0-rc.2): from the qBittorrent and Transmission containers (on - **Measured before** (the night's rig, 0.344.0-rc.2): from the qBittorrent and Transmission containers (on
@@ -13,6 +22,8 @@
login was refused (R-1126's gate, unchanged) — never traefik-public any more; one an earlier controller put on login was refused (R-1126's gate, unchanged) — never traefik-public any more; one an earlier controller put on
traefik-public leaves it. OnlyOffice joins `felhom-files` while — and only while — its FileBrowser link is on (it traefik-public leaves it. OnlyOffice joins `felhom-files` while — and only while — its FileBrowser link is on (it
fetches and saves documents at `http://filebrowser:80`, R-1018) — the one stated exception. The route stays by name. fetches and saves documents at `http://filebrowser:80`, R-1018) — the one stated exception. The route stays by name.
- **On the release every box re-creates FileBrowser once** (its rendered compose changed — a comment); files.<domain>
is away for a few seconds, then served on the files network (seen on the night's rig).
- Tests: `TestR1281_*` (three) + the two R-1126 gate tests updated to the files network. Red-proofs: felhom.eu - Tests: `TestR1281_*` (three) + the two R-1126 gate tests updated to the files network. Red-proofs: felhom.eu
`documentation/audits/night-2026-10-25/redproof-r1281.txt`. `documentation/audits/night-2026-10-25/redproof-r1281.txt`.
+20
View File
@@ -240,6 +240,26 @@ func (m *Manager) driveOfNamespace(nsRoot string) string {
return nsRoot return nsRoot
} }
// KeptBackupsFor (R-1278) counts the backups of an EARLIER install of stack that the box keeps: its set-aside units
// (D47) and the unit „remove — keep backups" left (a removed app's). The install page names them beside a fresh start.
func (m *Manager) KeptBackupsFor(stack string) int {
if m == nil || !validMirrorStackName(stack) {
return 0
}
n := 0
if m.settings != nil {
for _, k := range m.settings.GetKeptCopies() {
if k.Kind == KeptKindUnit && k.Source == stack {
n++
}
}
}
if _, ok := m.RemovedAppUnitFor(stack); ok {
n++
}
return n
}
// SetAsideDirsForApp lists the app's set-aside folders (<root>/backups/set-aside/<stack>) that exist, on every // SetAsideDirsForApp lists the app's set-aside folders (<root>/backups/set-aside/<stack>) that exist, on every
// namespace root a unit of it can sit on. // namespace root a unit of it can sit on.
func (m *Manager) SetAsideDirsForApp(stack string) []string { func (m *Manager) SetAsideDirsForApp(stack string) []string {
@@ -373,3 +373,21 @@ func TestR1288_ARestoreOfASetAsideUnitRetiresTheReplacedUnitQuietly(t *testing.T
t.Fatalf("the next backup set something aside again: alarms %v, set-aside %v", alarms, r.setAside(t)) t.Fatalf("the next backup set something aside again: alarms %v, set-aside %v", alarms, r.setAside(t))
} }
} }
// R-1278: the install page's count of an earlier install's kept backups — a set-aside unit counts once per copy.
func TestR1278_KeptBackupsForCountsTheSetAsideUnits(t *testing.T) {
r := d47Setup(t, "aaaa000000000001")
if n := r.m.KeptBackupsFor("tandoor"); n != 0 {
t.Fatalf("before any set-aside: %d", n)
}
r.reinstall("bbbb000000000002")
if err := r.m.captureRecoveryUnit("tandoor", false); err != nil {
t.Fatal(err)
}
if n := r.m.KeptBackupsFor("tandoor"); n != 1 {
t.Fatalf("after one set-aside: %d, want 1", n)
}
if n := r.m.KeptBackupsFor("other"); n != 0 {
t.Fatalf("another app counts nothing: %d", n)
}
}
+2
View File
@@ -1156,6 +1156,8 @@
"deploy.email_kuldes": "Sending e-mail", "deploy.email_kuldes": "Sending e-mail",
"deploy.email_kuldes_engedelyezese_ennel_az": "Allow sending e-mail for this app", "deploy.email_kuldes_engedelyezese_ennel_az": "Allow sending e-mail for this app",
"deploy.engedelyezett_orszagok_ehhez_az_alkalmaz": "Allowed countries for this app", "deploy.engedelyezett_orszagok_ehhez_az_alkalmaz": "Allowed countries for this app",
"deploy.kept_backup_offer": "The box kept a backup of an earlier install of this app. The new install starts empty; afterwards you can load the backup on the Restore page.",
"deploy.kept_backup_offer_link": "Open the kept backups",
"deploy.ennek_az_alkalmazasnak_van_korabbi": "This app has an earlier backup, so we filled in the fields below from its own backup.", "deploy.ennek_az_alkalmazasnak_van_korabbi": "This app has an earlier backup, so we filled in the fields below from its own backup.",
"deploy.ez_az_alkalmazas_mar_telepitve": "This app is already installed. The settings below are read-only.", "deploy.ez_az_alkalmazas_mar_telepitve": "This app is already installed. The settings below are read-only.",
"deploy.ez_az_alkalmazas_tud_emailt": "This app can send e-mail (for example password resets, invitations) through Felhom,\n without setting up a separate provider. The sender address: <strong>{{.AppEmailFromAddress}}</strong>.", "deploy.ez_az_alkalmazas_tud_emailt": "This app can send e-mail (for example password resets, invitations) through Felhom,\n without setting up a separate provider. The sender address: <strong>{{.AppEmailFromAddress}}</strong>.",
+2
View File
@@ -1151,6 +1151,8 @@
"deploy.email_kuldes": "Email-küldés", "deploy.email_kuldes": "Email-küldés",
"deploy.email_kuldes_engedelyezese_ennel_az": "Email-küldés engedélyezése ennél az alkalmazásnál", "deploy.email_kuldes_engedelyezese_ennel_az": "Email-küldés engedélyezése ennél az alkalmazásnál",
"deploy.engedelyezett_orszagok_ehhez_az_alkalmaz": "Engedélyezett országok ehhez az alkalmazáshoz", "deploy.engedelyezett_orszagok_ehhez_az_alkalmaz": "Engedélyezett országok ehhez az alkalmazáshoz",
"deploy.kept_backup_offer": "A doboz megőrizte ennek az alkalmazásnak egy korábbi telepítéséből származó mentését. Az új telepítés üresen indul; a mentést utána a Visszaállítás oldalon töltheted vissza.",
"deploy.kept_backup_offer_link": "A megőrzött mentések megnyitása",
"deploy.ennek_az_alkalmazasnak_van_korabbi": "Ennek az alkalmazásnak van korábbi mentése, ezért az alábbi mezőket a saját mentése alapján töltöttük ki.", "deploy.ennek_az_alkalmazasnak_van_korabbi": "Ennek az alkalmazásnak van korábbi mentése, ezért az alábbi mezőket a saját mentése alapján töltöttük ki.",
"deploy.ez_az_alkalmazas_mar_telepitve": "Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.", "deploy.ez_az_alkalmazas_mar_telepitve": "Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.",
"deploy.ez_az_alkalmazas_tud_emailt": "Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,\n külön szolgáltató beállítása nélkül. A feladó címe: <strong>{{.AppEmailFromAddress}}</strong>.", "deploy.ez_az_alkalmazas_tud_emailt": "Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,\n külön szolgáltató beállítása nélkül. A feladó címe: <strong>{{.AppEmailFromAddress}}</strong>.",
+2
View File
@@ -531,9 +531,11 @@ func (s *Server) deployPageData(w http.ResponseWriter, r *http.Request, name str
data["RestoreRecordedAddress"] = "" data["RestoreRecordedAddress"] = ""
data["RestoreRecordedDeclaresPath"] = declaresDataPath data["RestoreRecordedDeclaresPath"] = declaresDataPath
data["RestoreHasRecord"] = false data["RestoreHasRecord"] = false
data["KeptBackups"] = 0 // R-1278: backups of an earlier install the box keeps (set-aside or removed-app units)
// Part 1's visibility line needs the real path, not a literal in a template. // Part 1's visibility line needs the real path, not a literal in a template.
data["SystemDataPath"] = s.cfg.Paths.SystemDataPath data["SystemDataPath"] = s.cfg.Paths.SystemDataPath
if !alreadyDeployed && s.backupMgr != nil { if !alreadyDeployed && s.backupMgr != nil {
data["KeptBackups"] = s.backupMgr.KeptBackupsFor(name)
if place, addr, ok := s.backupMgr.RecordedUnitForStack(name); ok { if place, addr, ok := s.backupMgr.RecordedUnitForStack(name); ok {
prefill := map[string]string{} prefill := map[string]string{}
if addr.Known() { if addr.Known() {
@@ -181,6 +181,12 @@ func i18nCasesA() []i18nCase {
{"deploy_new", "deploy", func() map[string]interface{} { {"deploy_new", "deploy", func() map[string]interface{} {
return i18nDeployData(false, stacks.StateNotDeployed, 1) return i18nDeployData(false, stacks.StateNotDeployed, 1)
}}, }},
// R-1278 (2026-10-25 night): a backup of an earlier install is kept — the install page says so.
{"deploy_new_kept_backup_r1278", "deploy", func() map[string]interface{} {
d := i18nDeployData(false, stacks.StateNotDeployed, 1)
d["KeptBackups"] = 1
return d
}},
// R-1125 (v0.327.0, D10 = A): this page shows the same memory sentence — re-captured under this name. // R-1125 (v0.327.0, D10 = A): this page shows the same memory sentence — re-captured under this name.
{"deploy_new_blocked_r1125", "deploy", func() map[string]interface{} { {"deploy_new_blocked_r1125", "deploy", func() map[string]interface{} {
return i18nDeployData(false, stacks.StateNotDeployed, 2) return i18nDeployData(false, stacks.StateNotDeployed, 2)
@@ -0,0 +1,36 @@
package web
import (
"os"
"strings"
"testing"
)
// R-1278: a volume app's reinstall said nothing about the backup of the earlier install the box keeps (set aside by
// D47, or left by „remove — keep backups"). The install page now says the install starts empty and links the restore
// page. One render per branch of the gate (the template-gate rule): kept + not installed → shown; none → not shown;
// installed → not shown.
// RED-PROOF: delete the {{if .KeptBackups}} block → the first case fails.
func TestR1278_TheInstallPageOffersTheKeptBackup(t *testing.T) {
shown := renderDeployWithFields(t, false, map[string]interface{}{"KeptBackups": 2})
if !strings.Contains(shown, `data-kept-backups="2"`) || !strings.Contains(shown, `href="/backups/restore"`) {
t.Fatal("with a kept backup of an earlier install the page must say so and link the restore page")
}
if strings.Contains(renderDeployWithFields(t, false, map[string]interface{}{"KeptBackups": 0}), "kept-backup-offer") {
t.Fatal("no kept backup → no notice")
}
if strings.Contains(renderDeployWithFields(t, false, map[string]interface{}{"KeptBackups": 1, "AlreadyDeployed": true}), "kept-backup-offer") {
t.Fatal("an installed app shows no install-time notice")
}
}
// The page reads the count from the backup manager (seam-built-but-never-wired is a recorded class).
func TestR1278_TheDeployPageAsksTheBackupManager(t *testing.T) {
b, err := os.ReadFile("handlers.go")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), `data["KeptBackups"] = s.backupMgr.KeptBackupsFor(name)`) {
t.Fatal("deployPageData must set KeptBackups from the backup manager")
}
}
@@ -78,6 +78,15 @@
{{T "deploy.ha_most_mashova_szeretned_telepiteni"}} {{T "deploy.ha_most_mashova_szeretned_telepiteni"}}
</div> </div>
{{end}} {{end}}
{{- if and (not .AlreadyDeployed) .KeptBackups}}{{/* R-1278: a backup of an earlier install is kept (set aside, D47,
or left by „remove — keep backups"). The install starts empty; the backup is loaded afterwards on the
restore page („Megőrzött másolatok"). */}}
<div class="alert alert-info kept-backup-offer" data-kept-backups="{{.KeptBackups}}">
<svg class="ico ico-sm"><use href="#i-info"/></svg>
{{T "deploy.kept_backup_offer"}}
<a href="/backups/restore" class="alert-link">{{T "deploy.kept_backup_offer_link"}}</a>
</div>
{{- end}}
{{/* R-351 / Part 1 — WHERE THE DATA WILL LIVE, before the button is pressed. Measured {{/* R-351 / Part 1 — WHERE THE DATA WILL LIVE, before the button is pressed. Measured
2026-08-21: 13 of 53 catalog templates declare a storage field; the other 40 have none and 2026-08-21: 13 of 53 catalog templates declare a storage field; the other 40 have none and
their data goes to the system drive, which no screen said. This states it. It changes no their data goes to the system drive, which no screen said. This states it. It changes no
File diff suppressed because it is too large Load Diff