diff --git a/CONTEXT.md b/CONTEXT.md index 6e6599d..8ae0eac 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,7 @@ > > 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 ### felhom-controller (this repo) -- **Version:** v0.7.1 +- **Version:** v0.7.2 - **Phase 1:** ✅ COMPLETE — Stack Manager + Deploy Flow - **Phase 2:** ✅ COMPLETE — Monitoring & Health (scheduler, CPU/temp, healthchecks.io pings) - **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 - **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:** - **Three workstreams across two repos** (deploy-felhom-compose + felhom.eu): - **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 ### What's next (priorities) -1. **Manual steps for v0.7.1** — Viktor needs to: - - Rebuild + redeploy felhom-hub to k3s (hub code updated with notification endpoint + Resend integration) - - Configure `notifications.resend_api_key` in hub.yaml - - Set notification email in Settings → Értesítések on demo-felhom - - Test notification flow end-to-end (Settings → "Teszt email küldése") +1. **Deploy v0.7.2** — Build + deploy both hub (0.1.5) and controller (0.7.2): + - Hub must be deployed FIRST (controller needs /api/v1/preferences endpoint) + - Then build + deploy controller + - Test: save notification settings → hub logs "Notification preferences updated" → "Teszt email küldése" → email arrives + - 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 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) @@ -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 | | 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 | +| 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 | | 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 | diff --git a/controller/README.md b/controller/README.md index b910d26..9103abc 100644 --- a/controller/README.md +++ b/controller/README.md @@ -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 diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 274e788..240924d 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -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) diff --git a/controller/internal/notify/notifier.go b/controller/internal/notify/notifier.go index 75dd6f9..9c1cc08 100644 --- a/controller/internal/notify/notifier.go +++ b/controller/internal/notify/notifier.go @@ -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"` diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index 120f97d..016bfd6 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -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) }