v0.263.0: a failed update puts the app back by itself (09 decision 15, R-637)
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
The guarded update gains a folder copy of the app's named volumes, taken after the pull where the app stops anyway (decision 19, chosen by the 2026-09-23 bake-off). On a failed health check the box undoes: every copy validated by its finished-marker first, volumes refilled, definition and pin from the job's own pre-update copies, the old version checked with the OLD .felhom.yml probe. It holds only if the undo fails, and the hold sentence says so and what state the data is in. Bind-mounted folders are never touched. - R-637 built; R-638/R-640/R-641 do not arise with a folder copy; R-639 (pre-update copies incl. .felhom.yml kept until the undo is over). - journal phases copying/undoing with power-cut recovery. - app.yaml last_update_undone + one line on the app page (hu/en). - R-642: start/restart never answer "completed". - Removal deletes kept undo copies. MinAgent unchanged (0.131.0). Nine red-proofs in REPORT.md. 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:
@@ -833,6 +833,13 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
}
|
||||
}
|
||||
|
||||
// v0.263.0 (09 §3 decision 15): the box undid a failed update by itself — one line under the badge,
|
||||
// in the REQUEST's language (a finished sentence passed as page data must be rendered per request,
|
||||
// never taken from a Hungarian store — the composed-sentence class, R-573/R-590/R-596/R-598).
|
||||
if found.Deployed && found.AppConfig != nil && found.AppConfig.LastUpdateUndone != nil {
|
||||
data["UpdateUndoneLine"] = s.msg(r, "app_info.update_undone", found.Name, undoneWhen(found.AppConfig.LastUpdateUndone.At))
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, "app_info", data)
|
||||
}
|
||||
|
||||
@@ -3503,3 +3510,13 @@ func generateFileBrowserCompose(domain string, storageMounts []string) string {
|
||||
func generateFileBrowserConfig(paths []settings.StoragePath, importSource bool) string {
|
||||
return infra.RenderFileBrowserConfig(paths, importSource)
|
||||
}
|
||||
|
||||
// undoneWhen renders last_update_undone.at for the page line, in the box's time zone; the raw value
|
||||
// when it does not parse (a record is never hidden for its format).
|
||||
func undoneWhen(rfc3339 string) string {
|
||||
t, err := time.Parse(time.RFC3339, rfc3339)
|
||||
if err != nil {
|
||||
return rfc3339
|
||||
}
|
||||
return t.In(getTimezone()).Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
<div class="alert alert-error" style="margin-top:1rem" data-held="true">{{.Stack.HoldReason}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "app_info.mentesek"}}</a></div>
|
||||
{{else if .Stack.UpdateError}}
|
||||
<div class="alert alert-warning" style="margin-top:1rem" data-update-error="true">{{.Stack.UpdateError}}</div>
|
||||
{{else if .UpdateUndoneLine}}
|
||||
<div class="alert alert-info" style="margin-top:1rem" data-update-undone="true">{{.UpdateUndoneLine}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if .MissingStorageLabel}}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"html"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// v0.263.0 — after the box UNDID a failed update, the app page carries one line saying so, in the
|
||||
// REQUEST's language. Driven through the REAL handler (appDetailHandler) and the REAL template, both
|
||||
// languages, and the other language's sentence asserted GONE — the composed-sentence class
|
||||
// (R-573/R-590/R-596/R-598) is only found this way.
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT.md): drop the UpdateUndoneLine block from appDetailHandler — neither
|
||||
// language then carries the line, and this test fails on the first assertion.
|
||||
func TestUndo_PageSaysTheBoxPutTheAppBack(t *testing.T) {
|
||||
s, _ := credsHarness(t)
|
||||
sd := filepath.Join(s.cfg.Paths.StacksDir, "crafty")
|
||||
app := "deployed: true\nlast_update_undone:\n to:\n web: crafty:2.0.0\n at: \"2026-09-23T08:03:00Z\"\n why: \"not healthy\"\n"
|
||||
if err := os.WriteFile(filepath.Join(sd, "app.yaml"), []byte(app), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = s.stackMgr.ScanStacks()
|
||||
s.loadTemplates()
|
||||
|
||||
render := func(lang string) string {
|
||||
rr := httptest.NewRecorder()
|
||||
s.appDetailHandler(rr, httptest.NewRequest("GET", "/apps/crafty?lang="+lang, nil), "crafty")
|
||||
return html.UnescapeString(rr.Body.String())
|
||||
}
|
||||
en, hu := render("en"), render("hu")
|
||||
const enLine, huLine = "The box put back the previous version and its data automatically", "A doboz automatikusan visszaállította az előző változatot és az adatokat"
|
||||
if !strings.Contains(en, enLine) || !strings.Contains(en, "crafty") {
|
||||
t.Errorf("the English page must carry the undone line")
|
||||
}
|
||||
if strings.Contains(en, huLine) {
|
||||
t.Errorf("the Hungarian sentence must be GONE from the English page")
|
||||
}
|
||||
if !strings.Contains(hu, huLine) || strings.Contains(hu, enLine) {
|
||||
t.Errorf("the Hungarian page must carry the Hungarian line and not the English one")
|
||||
}
|
||||
if !strings.Contains(hu, "data-update-undone") {
|
||||
t.Errorf("the line must render in its own marked alert")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user