v0.279.0: after_install (decision 45), known default logins on the page, Part D empty-backup alarm, night chain (R-705), R-706
gates / gates (push) Successful in 27s

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-28 18:38:30 +02:00
parent 2e9a948cbd
commit 0c702f834a
35 changed files with 1119 additions and 16 deletions
+34 -10
View File
@@ -1191,6 +1191,18 @@ func main() {
// It rides the EXISTING per-run digest (backup_run_failures) rather than a new event type: a
// new type is a two-repo change (the hub's allowedEventTypes drops anything unlisted), and the
// digest is already operator-only and already means "this run did not fully do its job".
// Part D (v0.279.0): a RUNNING app whose newest copy holds no database dump and no volume tar. Rides
// the same operator-only digest as R-203 (no new event type — the hub drops unlisted ones), once per
// app per tier per day (the backup package dedupes).
backupMgr.SetHollowCopyNotify(func(app, tier string) {
d := notify.BackupRunFailuresDetails{RunKind: "data-check", Attempted: 1, Failed: 1,
Apps: []notify.RunFailureDetail{{App: app, Leg: "hollow-" + tier,
Reason: "the app is running but its newest " + tier + " copy holds no database dump and no volume tar"}}}
notifier.NotifyBackupRunFailures(fmt.Sprintf(
"Backup holds NO DATA: %s is running, but its newest %s copy carries no database dump and no volume tar — "+
"the app is not protected on that tier. Check for a hold on a running app (R-704's shape) or a skipped dump.",
app, tier), d)
})
backupMgr.SetOffboxGapNotify(func(gaps map[string][]string) {
apps := make([]string, 0, len(gaps))
for app := range gaps {
@@ -1749,19 +1761,31 @@ func main() {
// R-536: „Alkalmazás telepítve" is sent when the async deploy ACTUALLY finishes, and a deploy
// that ends badly now says so instead of being silence. The accept-time event is
// `app_deploy_started`, emitted by the API router beside its 202.
if notifier != nil {
stackMgr.SetDeployDoneHook(func(name string, ok bool, detail string) {
display := name
if s, found := stackMgr.GetStack(name); found && s.Meta.DisplayName != "" {
display = s.Meta.DisplayName
}
if ok {
// v0.279.0: the hook is set on EVERY box — after_install (decision 45) must run on a box with no hub too;
// only the events need the notifier.
stackMgr.SetDeployDoneHook(func(name string, ok bool, detail string) {
display := name
if s, found := stackMgr.GetStack(name); found && s.Meta.DisplayName != "" {
display = s.Meta.DisplayName
}
if ok {
if notifier != nil {
notifier.NotifyAppDeployed(name, display)
return
}
// v0.279.0 (decision 45): a FRESH install replaces a known default login with its generated one,
// when the template declares after_install. Never on a restore or a kept-data load — those do not
// pass through the deploy-done hook.
go func() {
if ran, err := stackMgr.RunAfterInstall(name, 10*time.Minute); ran && err != nil {
log.Printf("[WARN] [stacks] after_install %s: %v", name, err)
}
}()
return
}
if notifier != nil {
notifier.NotifyAppDeployFailed(name, display, detail)
})
}
}
})
// R-681 (v0.270.0): an install a restart cut off is finished through the failure path and reported —
// AFTER the deploy-done hook exists, so its event is sent.
if names := stackMgr.RecoverInterruptedInstalls(); len(names) > 0 {