feat: add controller self-update mechanism (v0.16.0)
New selfupdate package: version parsing, audit state file, updater with
Gitea registry V2 check, docker pull + compose rewrite + compose up flow.
- API: /api/selfupdate/{status,check,update} with session+bearer auth
- UI: Settings "Verzió és frissítés" card with check/install buttons + JS polling
- Scheduler: periodic check (6h default) + optional daily auto-update
- Notifications: success/failure on post-update startup verification
- Alert: info banner when update available
- docker-compose.yml: add directory bind mount for compose file access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ func NewAlertManager(logger *log.Logger) *AlertManager {
|
||||
|
||||
// Refresh regenerates alerts from the latest health check report and config state.
|
||||
// Called after each health check cycle (every 5 minutes).
|
||||
func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config, backupMgr *backup.Manager) {
|
||||
func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config, backupMgr *backup.Manager, updateAvailable bool, latestVersion string) {
|
||||
var alerts []Alert
|
||||
|
||||
// From health check issues (critical)
|
||||
@@ -97,6 +97,17 @@ func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config
|
||||
})
|
||||
}
|
||||
|
||||
// Update available
|
||||
if updateAvailable && latestVersion != "" {
|
||||
alerts = append(alerts, Alert{
|
||||
ID: "update-available",
|
||||
Level: "info",
|
||||
Message: fmt.Sprintf("Új controller verzió elérhető: %s", latestVersion),
|
||||
Link: "/settings",
|
||||
LinkText: "Frissítés",
|
||||
})
|
||||
}
|
||||
|
||||
// Sort: errors first, then warnings, then info
|
||||
sortAlerts(alerts)
|
||||
|
||||
|
||||
@@ -906,6 +906,25 @@ func (s *Server) settingsData() map[string]interface{} {
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
|
||||
data["HubEnabled"] = s.cfg.Hub.Enabled
|
||||
|
||||
// Self-update status
|
||||
data["SelfUpdateEnabled"] = s.cfg.SelfUpdate.Enabled
|
||||
if s.updater != nil {
|
||||
status := s.updater.GetStatus()
|
||||
data["UpdateRunning"] = status.Running
|
||||
if status.LastCheck != nil {
|
||||
data["UpdateAvailable"] = status.LastCheck.UpdateAvailable
|
||||
data["LatestVersion"] = status.LastCheck.LatestVersion
|
||||
data["LastCheckTime"] = status.LastCheck.CheckedAt
|
||||
data["LastCheckError"] = status.LastCheck.Error
|
||||
}
|
||||
if status.LastState != nil {
|
||||
data["LastUpdateState"] = status.LastState
|
||||
}
|
||||
data["AutoUpdateEnabled"] = s.cfg.SelfUpdate.AutoUpdate
|
||||
data["AutoUpdateTime"] = s.cfg.SelfUpdate.AutoUpdateTime
|
||||
}
|
||||
|
||||
data["NotificationPrefs"] = s.settings.GetNotificationPrefs()
|
||||
|
||||
// Storage paths with display data
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/notify"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/selfupdate"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
@@ -29,6 +30,7 @@ type Server struct {
|
||||
settings *settings.Settings
|
||||
alertManager *AlertManager
|
||||
notifier *notify.Notifier
|
||||
updater *selfupdate.Updater
|
||||
logger *log.Logger
|
||||
version string
|
||||
tmpl *template.Template
|
||||
@@ -49,7 +51,7 @@ type Server struct {
|
||||
restorePlan *backup.RestorePlan
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, crossDrive *backup.CrossDriveRunner, sched *scheduler.Scheduler, sett *settings.Settings, alertMgr *AlertManager, notif *notify.Notifier, logger *log.Logger, version string) *Server {
|
||||
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, crossDrive *backup.CrossDriveRunner, sched *scheduler.Scheduler, sett *settings.Settings, alertMgr *AlertManager, notif *notify.Notifier, updater *selfupdate.Updater, logger *log.Logger, version string) *Server {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
stackMgr: stackMgr,
|
||||
@@ -60,6 +62,7 @@ func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *syste
|
||||
settings: sett,
|
||||
alertManager: alertMgr,
|
||||
notifier: notif,
|
||||
updater: updater,
|
||||
logger: logger,
|
||||
version: version,
|
||||
sessions: make(map[string]*session),
|
||||
|
||||
@@ -56,13 +56,144 @@
|
||||
<span class="settings-label">Hub jelentés</span>
|
||||
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-green">✅ Aktív</span>{{else}}–{{end}}</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Controller verzió</span>
|
||||
<span class="settings-value mono">{{.Version}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section: Version & Update -->
|
||||
<div class="settings-card">
|
||||
<h3>Verzió és frissítés</h3>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Jelenlegi verzió</span>
|
||||
<span class="settings-value mono">{{.Version}}</span>
|
||||
</div>
|
||||
{{if .SelfUpdateEnabled}}
|
||||
{{if .LatestVersion}}
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Legújabb verzió</span>
|
||||
<span class="settings-value mono">
|
||||
{{.LatestVersion}}
|
||||
{{if .UpdateAvailable}}
|
||||
<span class="state-text-green" style="margin-left:0.5em;">● Frissítés elérhető</span>
|
||||
{{else}}
|
||||
<span style="margin-left:0.5em; color:#888;">— naprakész</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .LastCheckTime}}
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Utolsó ellenőrzés</span>
|
||||
<span class="settings-value mono">{{.LastCheckTime}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .LastCheckError}}
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Hiba</span>
|
||||
<span class="settings-value state-text-red">{{.LastCheckError}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Automatikus frissítés</span>
|
||||
<span class="settings-value">
|
||||
{{if .AutoUpdateEnabled}}<span class="state-text-green">✅ Aktív</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}–{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{with .LastUpdateState}}
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Utolsó frissítés</span>
|
||||
<span class="settings-value">
|
||||
{{if eq .Status "success"}}<span class="state-text-green">✅ Sikeres</span> ({{.PreviousVersion}} → {{.TargetVersion}})
|
||||
{{else if eq .Status "failed"}}<span class="state-text-red">❌ Sikertelen</span> — {{.Error}}
|
||||
{{else if eq .Status "pending"}}<span class="state-text-yellow">⏳ Folyamatban</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="settings-row" style="padding-top: 0.5em;">
|
||||
<span class="settings-label"></span>
|
||||
<span class="settings-value">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">Frissítés keresése</button>
|
||||
{{if .UpdateAvailable}}
|
||||
<button class="btn btn-primary btn-sm" id="btn-trigger-update" onclick="triggerUpdate()" style="margin-left:0.5em;">Frissítés telepítése</button>
|
||||
{{end}}
|
||||
<span id="update-status-msg" style="margin-left:0.5em; display:none;"></span>
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function checkUpdate() {
|
||||
var btn = document.getElementById('btn-check-update');
|
||||
var msg = document.getElementById('update-status-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Ellenőrzés...';
|
||||
msg.style.display = 'none';
|
||||
fetch('/api/selfupdate/check', {method:'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
msg.textContent = data.error || 'Hiba történt';
|
||||
msg.style.display = 'inline';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Frissítés keresése';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msg.textContent = 'Kapcsolódási hiba';
|
||||
msg.style.display = 'inline';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Frissítés keresése';
|
||||
});
|
||||
}
|
||||
|
||||
function triggerUpdate() {
|
||||
if (!confirm('Biztosan frissíti a controllert?\n\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.')) return;
|
||||
var btn = document.getElementById('btn-trigger-update');
|
||||
var checkBtn = document.getElementById('btn-check-update');
|
||||
var msg = document.getElementById('update-status-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Frissítés...';
|
||||
if (checkBtn) checkBtn.disabled = true;
|
||||
msg.textContent = 'Frissítés folyamatban...';
|
||||
msg.style.display = 'inline';
|
||||
fetch('/api/selfupdate/update', {method:'POST'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
msg.textContent = 'Újraindulás...';
|
||||
pollUntilBack();
|
||||
} else {
|
||||
msg.textContent = data.error || 'Hiba történt';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Frissítés telepítése';
|
||||
if (checkBtn) checkBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msg.textContent = 'Kapcsolódási hiba';
|
||||
pollUntilBack();
|
||||
});
|
||||
}
|
||||
|
||||
function pollUntilBack() {
|
||||
var iv = setInterval(function() {
|
||||
fetch('/api/health')
|
||||
.then(function(r) {
|
||||
if (r.ok) {
|
||||
clearInterval(iv);
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Section: Storage Paths -->
|
||||
<div class="settings-card">
|
||||
<h3>Adattárolók</h3>
|
||||
|
||||
Reference in New Issue
Block a user