kept data: the choice at reinstall, the list, the read-only view, the load (09 decision 36); R-690 fixed
gates / gates (push) Successful in 26s

An install over an app's kept drive folder (appdata/<app> non-empty) asks the household:
"use my kept data" (a load from the newest copy of THIS drive's install, own unit or
second-drive mirror, then the template's after_load) or "start fresh" (the folder is
renamed into <drive>/kept/<app>/<date>/ with the removed app's unit; nothing deleted).
The install API answers 409 kept_data_choice until one is chosen; DeployStack refuses
too. New page Megorzott adatok / Kept data (/kept-data): Load / Look / Delete (typed
confirmation, the only deletion of kept data). FileBrowser gets a read-only source.
The drive-full warning names the kept folders. <drive>/kept is protected and outside
every backup leg.

R-690: the removed-app restore (R-487) never found a unit on a DATA drive — it asked
GetStackComposePath (true for every catalog app) and restored nextcloud with no env.
Now isStackDeployed; pinned with a production-shaped provider.

Red-proofs: audits/night-2026-09-26/E/redproofs/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-25 13:32:30 +02:00
parent 5a731b45b9
commit 43e99d160c
44 changed files with 3311 additions and 37 deletions
+38
View File
@@ -191,8 +191,18 @@ type InstalledImage struct {
type DeployRequest struct {
StackName string `json:"stack_name"`
Values map[string]string `json:"values"` // env_var -> user-provided value
// KeptData is the household's answer when the app's drive folder already holds old data
// (`09` §3 decision 36): KeptChoiceFresh here; KeptChoiceUse is carried out by the API as a load
// from a backup and never reaches DeployStack. "" = no choice was made — refused when old data exists.
KeptData string `json:"kept_data,omitempty"`
}
// Kept-data choices at install (`09` §3 decision 36).
const (
KeptChoiceUse = "use"
KeptChoiceFresh = "fresh"
)
// DeployStack handles first-time deployment of an app.
// Returns a warning message (empty if none) and an error if deployment is blocked.
// 1. Check available memory against app requirements
@@ -358,6 +368,34 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
}
}
// `09` §3 decision 36 (R-657): an install NEVER runs into an app's old data silently. When the app's
// private drive folder already holds something, the household chooses: „start fresh" moves it into a
// dated kept folder (a rename on the same drive — nothing is deleted); „use my kept data" is a load
// from a backup, which the API performs instead of an install. No choice → refused, nothing moved.
// Pinned by TestKept_DeployRefusesOverOldDataWithoutAChoice.
if old := OldAppDataPaths(stack.ComposePath, env["HDD_PATH"]); len(old) > 0 {
switch req.KeptData {
case KeptChoiceFresh:
unit := ""
if m.keptUnitFn != nil {
unit = m.keptUnitFn(req.StackName, env["HDD_PATH"])
}
kept, err := m.KeepAside(req.StackName, env["HDD_PATH"], old, unit, time.Now())
if err != nil {
clearDeploying()
return "", fmt.Errorf("start fresh: %w", err)
}
m.logger.Printf("[INFO] [stacks] Deploy %s: start fresh — the old data (%v) is kept in %s", req.StackName, old, kept)
case "":
clearDeploying()
m.logger.Printf("[WARN] [stacks] Deploy %s REFUSED: the drive already holds its old data %v and no choice was made", req.StackName, old)
return "", util.KindErrorf(ErrKeptDataChoice, "the drive already holds %s's old data (%s): choose use or fresh", req.StackName, strings.Join(old, ", "))
default:
clearDeploying()
return "", util.KindErrorf(ErrKeptDataChoice, "kept_data %q is not an install choice here (use is a load from a backup)", req.KeptData)
}
}
// Save app.yaml.
// CTRL-T2-1: persist the env now, but mark the ON-DISK state Deployed:false
// until `docker compose up -d` actually succeeds (done in runComposeDeploy).