fix(rolling-update): stable done-detection, mobile banner layout, stale-state protection

- FinishRollingUpdateIfPending() auf API-Startup: transitiert
  updating-primary → done damit der UI-Flow nach Restart abschließt
- RollingUpdateStatus: setzt done nach Auslieferung auf idle zurück
  (verhindert Stale-done bei Page-Reload)
- wasRollingActiveRef: reagiert auf done nur wenn rolling in DIESER
  Session aktiv war — kein sofortiger Reload bei Stale-State
- UI-Fallback für updating-primary: poll auf /system/health version-flip
- Cluster-Erkennung via /cluster/status; Rolling-Update-Button nur im Cluster
- Update-Banner-Button nicht mehr gequetscht (flex-shrink:0 + nowrap)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-30 12:56:20 +02:00
parent bc6db1fc2b
commit 884f84d3f1
4 changed files with 85 additions and 77 deletions

View File

@@ -22,9 +22,24 @@ const (
phaseUpdatingSecondary = "updating-secondary"
phaseWaitingSecondary = "waiting-secondary"
phaseUpdatingPrimary = "updating-primary"
phaseDone = "done"
phaseFailed = "failed"
)
// FinishRollingUpdateIfPending wird beim API-Start aufgerufen. Wenn die
// State-Datei "updating-primary" enthält, bedeutet das dass der Primary
// gerade erfolgreich neugestartet ist → Update abgeschlossen → "done" schreiben.
func FinishRollingUpdateIfPending() {
st := readRollingUpdateState()
if st.Phase == phaseUpdatingPrimary {
writeRollingUpdateState(RollingUpdateState{
Phase: phaseDone,
SecondaryID: st.SecondaryID,
SecondaryFQDN: st.SecondaryFQDN,
})
}
}
// RollingUpdateState hält den Fortschritt des Rolling-Updates.
// Persistiert in rollingUpdateStateFile damit der Status über
// einen kurzen API-Neustart hinaus lesbar bleibt.
@@ -75,7 +90,7 @@ func (h *ClusterHandler) RollingUpdate(c *gin.Context) {
}
st := readRollingUpdateState()
if st.Phase != phaseIdle && st.Phase != phaseFailed {
if st.Phase != phaseIdle && st.Phase != phaseFailed && st.Phase != phaseDone {
response.OK(c, st)
return
}
@@ -112,11 +127,14 @@ func (h *ClusterHandler) RollingUpdate(c *gin.Context) {
}
// RollingUpdateStatus gibt den aktuellen Rolling-Update-State zurück.
// Wenn phase == "updating-primary" soll der Client auf /system/health
// umschalten (der Primary restartet gleich → State kann nicht mehr
// geschrieben werden).
// Bei phase == "done" wird nach Auslieferung sofort auf idle zurückgesetzt
// damit der nächste Pageload keinen Stale-done vorfindet.
func (h *ClusterHandler) RollingUpdateStatus(c *gin.Context) {
response.OK(c, readRollingUpdateState())
st := readRollingUpdateState()
response.OK(c, st)
if st.Phase == phaseDone {
writeRollingUpdateState(RollingUpdateState{Phase: phaseIdle})
}
}
func (h *ClusterHandler) runRollingUpdate(secondary *models.HANode) {
@@ -221,7 +239,7 @@ rm -f /var/lib/edgeguard/upgrade.sh
_ = exec.Command("sudo", "-n", "/usr/bin/systemctl", "reset-failed", unitName).Run()
cmd := exec.Command("sudo", "-n", "/usr/bin/systemd-run",
"--unit="+unitName,
"--description=EdgeGuard rolling-update (primary)",
"--description=EdgeGuard self-upgrade",
"--collect",
"bash", scriptPath)
if err := cmd.Run(); err != nil {