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:
+38
-8
@@ -7,7 +7,7 @@
|
|||||||
>
|
>
|
||||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||||
|
|
||||||
Last updated: 2026-02-16 (session 23)
|
Last updated: 2026-02-16 (session 24)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ Last updated: 2026-02-16 (session 23)
|
|||||||
## Current project state
|
## Current project state
|
||||||
|
|
||||||
### felhom-controller (this repo)
|
### felhom-controller (this repo)
|
||||||
- **Version:** v0.7.1
|
- **Version:** v0.7.2
|
||||||
- **Phase 1:** ✅ COMPLETE — Stack Manager + Deploy Flow
|
- **Phase 1:** ✅ COMPLETE — Stack Manager + Deploy Flow
|
||||||
- **Phase 2:** ✅ COMPLETE — Monitoring & Health (scheduler, CPU/temp, healthchecks.io pings)
|
- **Phase 2:** ✅ COMPLETE — Monitoring & Health (scheduler, CPU/temp, healthchecks.io pings)
|
||||||
- **Phase 3:** ✅ COMPLETE — Backups (DB dumps, restic integration, manual trigger, **dedicated backup page**)
|
- **Phase 3:** ✅ COMPLETE — Backups (DB dumps, restic integration, manual trigger, **dedicated backup page**)
|
||||||
@@ -33,7 +33,36 @@ Last updated: 2026-02-16 (session 23)
|
|||||||
- **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080
|
- **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080
|
||||||
- **All Phase 1-5 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth, monitoring, backups, backup detail page, system monitoring page, settings page
|
- **All Phase 1-5 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth, monitoring, backups, backup detail page, system monitoring page, settings page
|
||||||
|
|
||||||
### What was just completed (2026-02-16 session 23)
|
### What was just completed (2026-02-16 session 24)
|
||||||
|
- **v0.7.2 — Fix Notification Preferences Sync (Controller → Hub):**
|
||||||
|
- **Two repos changed** (deploy-felhom-compose + felhom.eu):
|
||||||
|
- **Hub: `POST /api/v1/preferences` endpoint** (`hub/internal/api/handler.go`):
|
||||||
|
- New route in API handler: same Bearer token auth as /report and /notify
|
||||||
|
- Accepts JSON payload: `{customer_id, email, enabled_events}`
|
||||||
|
- Calls existing `store.SaveNotificationPrefs()` — no store changes needed
|
||||||
|
- Logs preference updates at INFO level
|
||||||
|
- **Hub: Notification section on customer detail page** (`hub/internal/web/`, `hub/internal/store/store.go`):
|
||||||
|
- New `GetRecentNotifications()` store method returns last N notification_log entries
|
||||||
|
- `handleCustomerDetail()` loads NotifPrefs + RecentNotifications
|
||||||
|
- `joinStrings` template function added for event list display
|
||||||
|
- `customer.html` template: new "Notifications" section showing email, events, and last 10 notification log entries (time, event, status, message)
|
||||||
|
- **Controller: `SyncPreferences` method** (`internal/notify/notifier.go`):
|
||||||
|
- New `preferencesRequest` struct for JSON payload
|
||||||
|
- `SyncPreferences(email, enabledEvents)` — synchronous POST to hub `/api/v1/preferences`
|
||||||
|
- `IsEnabled()` getter for checking hub connectivity
|
||||||
|
- Hungarian error messages for user-facing feedback
|
||||||
|
- **Controller: Sync on settings save** (`internal/web/handlers.go`):
|
||||||
|
- `settingsNotificationsHandler` now calls `SyncPreferences` after saving to `settings.json`
|
||||||
|
- Three flash message variants: success (synced), warning (local save OK, sync failed), error (save failed)
|
||||||
|
- Local save always succeeds even if hub sync fails
|
||||||
|
- **Controller: Sync on startup** (`cmd/controller/main.go`):
|
||||||
|
- Non-blocking goroutine syncs preferences to hub when controller starts
|
||||||
|
- Only runs if hub is enabled and email is configured
|
||||||
|
- Handles hub DB rebuild recovery (re-populates preferences after hub redeployment)
|
||||||
|
- **Files changed**: hub (3 files: handler.go, store.go, server.go, customer.html), controller (3 files: notifier.go, handlers.go, main.go)
|
||||||
|
- **Documentation**: README.md updated (version, notify module, phase checklist), CONTEXT.md updated
|
||||||
|
|
||||||
|
### What was previously completed (2026-02-16 session 23)
|
||||||
- **v0.7.1 — Phase 2: Monitoring Warnings, Dashboard Alerts & Notification System:**
|
- **v0.7.1 — Phase 2: Monitoring Warnings, Dashboard Alerts & Notification System:**
|
||||||
- **Three workstreams across two repos** (deploy-felhom-compose + felhom.eu):
|
- **Three workstreams across two repos** (deploy-felhom-compose + felhom.eu):
|
||||||
- **Monitoring page "Távoli monitoring" section** (`monitoring.html`, `handlers.go`):
|
- **Monitoring page "Távoli monitoring" section** (`monitoring.html`, `handlers.go`):
|
||||||
@@ -469,11 +498,11 @@ Last updated: 2026-02-16 (session 23)
|
|||||||
7. Documentation: restart vs up -d for image updates
|
7. Documentation: restart vs up -d for image updates
|
||||||
|
|
||||||
### What's next (priorities)
|
### What's next (priorities)
|
||||||
1. **Manual steps for v0.7.1** — Viktor needs to:
|
1. **Deploy v0.7.2** — Build + deploy both hub (0.1.5) and controller (0.7.2):
|
||||||
- Rebuild + redeploy felhom-hub to k3s (hub code updated with notification endpoint + Resend integration)
|
- Hub must be deployed FIRST (controller needs /api/v1/preferences endpoint)
|
||||||
- Configure `notifications.resend_api_key` in hub.yaml
|
- Then build + deploy controller
|
||||||
- Set notification email in Settings → Értesítések on demo-felhom
|
- Test: save notification settings → hub logs "Notification preferences updated" → "Teszt email küldése" → email arrives
|
||||||
- Test notification flow end-to-end (Settings → "Teszt email küldése")
|
- Verify: hub customer detail page shows notification email + events + log
|
||||||
2. **Test alert banners** — Configure some missing ping UUIDs or disable backup to verify yellow/red banners appear
|
2. **Test alert banners** — Configure some missing ping UUIDs or disable backup to verify yellow/red banners appear
|
||||||
3. **Test backup flow** — trigger manual backup via dashboard, verify restic repo + DB dumps
|
3. **Test backup flow** — trigger manual backup via dashboard, verify restic repo + DB dumps
|
||||||
4. Add `app_info` + `optional_config` to more apps (start with Immich, Mealie, Vaultwarden)
|
4. Add `app_info` + `optional_config` to more apps (start with Immich, Mealie, Vaultwarden)
|
||||||
@@ -517,6 +546,7 @@ Last updated: 2026-02-16 (session 23)
|
|||||||
| In-memory notification cooldowns | Per-event-type cooldown map (default 6h). Lost on restart = acceptable (better to re-notify than miss). No persistence needed |
|
| In-memory notification cooldowns | Per-event-type cooldown map (default 6h). Lost on restart = acceptable (better to re-notify than miss). No persistence needed |
|
||||||
| Health status change detection | Only notify on degradation (ok→warn, ok→fail, warn→fail). Avoids spam on flapping. First run records baseline, doesn't notify |
|
| Health status change detection | Only notify on degradation (ok→warn, ok→fail, warn→fail). Avoids spam on flapping. First run records baseline, doesn't notify |
|
||||||
| Resend HTTP API (no SMTP) | Direct POST to api.resend.com — same pattern as website contact-mailer. Simpler than SMTP setup, good deliverability |
|
| Resend HTTP API (no SMTP) | Direct POST to api.resend.com — same pattern as website contact-mailer. Simpler than SMTP setup, good deliverability |
|
||||||
|
| Preferences sync on save + startup | Controller pushes prefs to hub (not pull). Startup sync handles hub DB rebuild. Local save always succeeds even if sync fails |
|
||||||
| Chart.js embedded locally | Customer hardware may not have internet — CDN not reliable for offline environments |
|
| Chart.js embedded locally | Customer hardware may not have internet — CDN not reliable for offline environments |
|
||||||
| Metrics downsampling via SQL | Bucket-based AVG in GROUP BY keeps Chart.js responsive with up to 30 days of data |
|
| Metrics downsampling via SQL | Bucket-based AVG in GROUP BY keeps Chart.js responsive with up to 30 days of data |
|
||||||
| 60s metrics collection interval | Good balance of resolution vs. storage — ~44K rows/month for system metrics |
|
| 60s metrics collection interval | Good balance of resolution vs. storage — ~44K rows/month for system metrics |
|
||||||
|
|||||||
+11
-3
@@ -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
|
with Traefik routing and health checks. The dashboard correctly shows real-time container states
|
||||||
including health substatus (starting → healthy → running).
|
including health substatus (starting → healthy → running).
|
||||||
|
|
||||||
Current version: **v0.6.1**
|
Current version: **v0.7.2**
|
||||||
|
|
||||||
### What works
|
### What works
|
||||||
- Dashboard with live container state (green/orange/yellow/red)
|
- 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)
|
- Heartbeat ping (5-minute "I'm alive" signal to Healthchecks)
|
||||||
- Weekly backup integrity check (restic check, Sunday 04:00)
|
- Weekly backup integrity check (restic check, Sunday 04:00)
|
||||||
- Central hub reporting (periodic JSON push to felhom-hub service)
|
- 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
|
### 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)
|
- 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)
|
│ │ ├── collector.go # Background collector (60s interval, system + docker stats)
|
||||||
│ │ ├── types.go # SystemSample, ContainerSample, StaticSystemInfo structs
|
│ │ ├── types.go # SystemSample, ContainerSample, StaticSystemInfo structs
|
||||||
│ │ └── sysinfo.go # Host-level static info (/proc, /etc)
|
│ │ └── sysinfo.go # Host-level static info (/proc, /etc)
|
||||||
|
│ ├── notify/
|
||||||
|
│ │ └── notifier.go # Notification relay (hub → email), preferences sync, cooldowns
|
||||||
│ ├── report/
|
│ ├── report/
|
||||||
│ │ ├── types.go # Hub report JSON payload definitions
|
│ │ ├── types.go # Hub report JSON payload definitions
|
||||||
│ │ ├── builder.go # Builds report from system/stacks/backup/metrics state
|
│ │ ├── 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) |
|
| **Scheduler** | `internal/scheduler/` | ✅ Done | Central job scheduler (periodic + daily, skip-if-running) |
|
||||||
| **Monitor** | `internal/monitor/` | ✅ Done | Healthchecks.io pings, system health checks |
|
| **Monitor** | `internal/monitor/` | ✅ Done | Healthchecks.io pings, system health checks |
|
||||||
| **Metrics** | `internal/metrics/` | ✅ Done | SQLite time-series store, system + container collection |
|
| **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) |
|
| **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 |
|
| **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] Heartbeat ping (5-minute "I'm alive" signal)
|
||||||
- [x] SQLite metrics store (system + container metrics, 60s collection, 30-day prune)
|
- [x] SQLite metrics store (system + container metrics, 60s collection, 30-day prune)
|
||||||
- [x] Backup integrity check (weekly restic check with Healthchecks ping)
|
- [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
|
### Phase 3 — Backups ✅ COMPLETE
|
||||||
- [x] DB auto-discovery (PostgreSQL/MariaDB containers via docker inspect)
|
- [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] Hub service (felhom-hub: REST API + SQLite + dark-theme dashboard)
|
||||||
- [x] K8s manifests for hub deployment on k3s
|
- [x] K8s manifests for hub deployment on k3s
|
||||||
- [ ] Fleet-wide update management
|
- [ ] 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
|
## Related Repositories
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
// Initial alert refresh (so alerts appear immediately, not after first 5min health check)
|
||||||
go func() {
|
go func() {
|
||||||
report := monitor.RunHealthCheck(cfg, cpuCollector)
|
report := monitor.RunHealthCheck(cfg, cpuCollector)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"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.
|
// notifyRequest is the JSON payload sent to the hub.
|
||||||
type notifyRequest struct {
|
type notifyRequest struct {
|
||||||
CustomerID string `json:"customer_id"`
|
CustomerID string `json:"customer_id"`
|
||||||
|
|||||||
@@ -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)
|
s.logger.Printf("[INFO] Notification preferences updated: email=%s, events=%v", email, enabledEvents)
|
||||||
|
|
||||||
|
// Sync preferences to hub
|
||||||
data := s.settingsData()
|
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)
|
s.render(w, "settings", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user