Controller v0.7.2: notification preferences sync to hub

- SyncPreferences() method on Notifier: POST to hub /api/v1/preferences
- IsEnabled() getter for hub connectivity check
- settingsNotificationsHandler: sync to hub after local save (3 flash message variants)
- Startup sync: non-blocking goroutine pushes prefs to hub on boot (DB rebuild recovery)
- Updated CONTEXT.md, README.md with v0.7.2 changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 20:18:22 +01:00
parent 8f5962c47d
commit 2649297096
5 changed files with 127 additions and 12 deletions
+11 -3
View File
@@ -24,7 +24,7 @@ controller generates secrets, saves app.yaml, runs `docker compose up -d`, and t
with Traefik routing and health checks. The dashboard correctly shows real-time container states
including health substatus (starting → healthy → running).
Current version: **v0.6.1**
Current version: **v0.7.2**
### What works
- Dashboard with live container state (green/orange/yellow/red)
@@ -59,6 +59,8 @@ Current version: **v0.6.1**
- Heartbeat ping (5-minute "I'm alive" signal to Healthchecks)
- Weekly backup integrity check (restic check, Sunday 04:00)
- Central hub reporting (periodic JSON push to felhom-hub service)
- Notification preferences sync to hub (controller → hub on save + startup)
- Notification system with email delivery via Resend API (hub relay)
### Known issues / next priorities
- Cloudflare Tunnel + Traefik TLS: paperless.demo-felhom.eu works locally but shows "Not secure" (certificate chain not fully validated through tunnel)
@@ -133,6 +135,8 @@ controller/
│ │ ├── collector.go # Background collector (60s interval, system + docker stats)
│ │ ├── types.go # SystemSample, ContainerSample, StaticSystemInfo structs
│ │ └── sysinfo.go # Host-level static info (/proc, /etc)
│ ├── notify/
│ │ └── notifier.go # Notification relay (hub → email), preferences sync, cooldowns
│ ├── report/
│ │ ├── types.go # Hub report JSON payload definitions
│ │ ├── builder.go # Builds report from system/stacks/backup/metrics state
@@ -173,6 +177,7 @@ controller/
| **Scheduler** | `internal/scheduler/` | ✅ Done | Central job scheduler (periodic + daily, skip-if-running) |
| **Monitor** | `internal/monitor/` | ✅ Done | Healthchecks.io pings, system health checks |
| **Metrics** | `internal/metrics/` | ✅ Done | SQLite time-series store, system + container collection |
| **Notify** | `internal/notify/` | ✅ Done | Notification relay to hub, preferences sync, cooldown tracking |
| **Report** | `internal/report/` | ✅ Done | Central hub push (JSON report builder + HTTP pusher) |
| **Backup** | `internal/backup/` | ✅ Done | DB auto-discovery + dump, restic snapshots, prune, manual trigger |
@@ -463,7 +468,8 @@ docker compose up -d
- [x] Heartbeat ping (5-minute "I'm alive" signal)
- [x] SQLite metrics store (system + container metrics, 60s collection, 30-day prune)
- [x] Backup integrity check (weekly restic check with Healthchecks ping)
- [ ] Customer notifications (email/Telegram)
- [x] Customer notifications (email via hub relay + Resend API)
- [x] Notification preferences sync (controller → hub on save + startup)
### Phase 3 — Backups ✅ COMPLETE
- [x] DB auto-discovery (PostgreSQL/MariaDB containers via docker inspect)
@@ -496,7 +502,9 @@ docker compose up -d
- [x] Hub service (felhom-hub: REST API + SQLite + dark-theme dashboard)
- [x] K8s manifests for hub deployment on k3s
- [ ] Fleet-wide update management
- [ ] Customer notifications (email/Telegram)
- [x] Customer notifications (email via hub relay)
- [x] Notification preferences sync (controller → hub)
- [x] Hub customer detail page: notification preferences + log display
## Related Repositories
+12
View File
@@ -237,6 +237,18 @@ func main() {
}()
}
// Sync notification preferences to hub on startup (handles hub DB rebuild recovery)
if notifier.IsEnabled() {
go func() {
prefs := sett.GetNotificationPrefs()
if prefs.Email != "" {
if err := notifier.SyncPreferences(prefs.Email, prefs.EnabledEvents); err != nil {
logger.Printf("[WARN] Failed to sync notification preferences on startup: %v", err)
}
}
}()
}
// Initial alert refresh (so alerts appear immediately, not after first 5min health check)
go func() {
report := monitor.RunHealthCheck(cfg, cpuCollector)
+55
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
@@ -51,6 +52,60 @@ func New(hubURL, apiKey, customerID string, sett *settings.Settings, logger *log
}
}
// IsEnabled returns whether the notifier has a configured hub connection.
func (n *Notifier) IsEnabled() bool {
return n.enabled
}
// preferencesRequest is the JSON payload sent to the hub preferences endpoint.
type preferencesRequest struct {
CustomerID string `json:"customer_id"`
Email string `json:"email"`
EnabledEvents []string `json:"enabled_events"`
}
// SyncPreferences pushes the current notification preferences to the hub.
// Called after the user saves notification settings on the settings page.
// Synchronous — returns error for the handler to display to the user.
func (n *Notifier) SyncPreferences(email string, enabledEvents []string) error {
if !n.enabled {
return fmt.Errorf("hub nem konfigurált")
}
payload := preferencesRequest{
CustomerID: n.customerID,
Email: email,
EnabledEvents: enabledEvents,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
url := n.hubURL + "/api/v1/preferences"
req, err := http.NewRequest("POST", url, bytes.NewReader(jsonData))
if err != nil {
return fmt.Errorf("request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+n.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := n.httpClient.Do(req)
if err != nil {
return fmt.Errorf("hub elérhetetlen: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return fmt.Errorf("hub hiba (%d): %s", resp.StatusCode, string(body))
}
n.logger.Printf("[INFO] Notification preferences synced to hub: email=%s, events=%v", email, enabledEvents)
return nil
}
// notifyRequest is the JSON payload sent to the hub.
type notifyRequest struct {
CustomerID string `json:"customer_id"`
+11 -1
View File
@@ -371,8 +371,18 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
s.logger.Printf("[INFO] Notification preferences updated: email=%s, events=%v", email, enabledEvents)
// Sync preferences to hub
data := s.settingsData()
data["NotificationSuccess"] = "Értesítési beállítások mentve."
if s.notifier != nil && s.notifier.IsEnabled() {
if err := s.notifier.SyncPreferences(email, enabledEvents); err != nil {
s.logger.Printf("[WARN] Failed to sync preferences to hub: %v", err)
data["NotificationSuccess"] = fmt.Sprintf("Értesítési beállítások mentve (helyi). A központi szinkronizálás sikertelen: %v", err)
} else {
data["NotificationSuccess"] = "Értesítési beállítások mentve."
}
} else {
data["NotificationSuccess"] = "Értesítési beállítások mentve."
}
s.render(w, "settings", data)
}