From f54d1a23de4922a8bed9e29286e3a8a7150a124a Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 19 Feb 2026 09:45:40 +0100 Subject: [PATCH] v0.15.4: Hub disabled notification, PushOnce, ReportingDisabled field --- CHANGELOG.md | 10 ++++++ CONTEXT.md | 4 +-- controller/README.md | 2 +- controller/cmd/controller/main.go | 48 ++++++++++++++++++++-------- controller/internal/report/pusher.go | 36 +++++++++++++++++++++ controller/internal/report/types.go | 1 + 6 files changed, 84 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a923d..5dd4295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## Changelog +### What was just completed (2026-02-19 session 54) +- **v0.15.4 (controller) + hub v0.1.6 — Hub reporting improvements:** + + **Controller:** When `hub.enabled: false` but URL+API key are configured, the controller now creates the `Pusher` and sends a one-time "disabled" notification on startup (`health.status = "disabled"`, `reporting_disabled: true`). This replaces the old behavior where a disabled controller was indistinguishable from a crashed node. Added `PushOnce()` method to `Pusher` (bypasses the `enabled` flag). Added `ReportingDisabled` field to the `Report` struct. + + **Hub:** Added "disabled" status handling — when the latest report has `health_status = "disabled"`, the overall status is "disabled" (checked BEFORE the stale-time logic, so it stays "PAUSED" even after 30min+). Dashboard shows gray "PAUSED" badge. Customer detail shows "Reporting has been disabled on this node" with a hint to re-enable. Storage labels now shown (`label` field with fallback to `mount`). Report history timestamps now show date + time ("Feb 19 09:46" instead of "09:46:54"). New `.status-badge-disabled` CSS (neutral gray `#475569`). + + **Files modified (controller):** `internal/report/types.go`, `internal/report/pusher.go`, `cmd/controller/main.go` + **Files modified (hub):** `hub/internal/web/server.go`, `hub/internal/web/templates/dashboard.html`, `hub/internal/web/templates/customer.html`, `hub/internal/web/templates/style.css` + ### What was just completed (2026-02-19 session 53) - **v0.15.3 — Show all storage paths on dashboard + fix hub report:** diff --git a/CONTEXT.md b/CONTEXT.md index b68ba76..4d7c8ef 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-17 (session 35) +Last updated: 2026-02-19 (session 54) --- @@ -20,7 +20,7 @@ Last updated: 2026-02-17 (session 35) - Customer deployments use Docker Compose (not Kubernetes) for simplicity ### felhom-controller (this repo) -- **Version:** v0.11.9 +- **Version:** v0.15.4 - **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**) diff --git a/controller/README.md b/controller/README.md index fdca48c..f6dd249 100644 --- a/controller/README.md +++ b/controller/README.md @@ -4,7 +4,7 @@ A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware. -**Current version: v0.14.1** +**Current version: v0.15.4** --- diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 9811f40..fa42505 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -247,17 +247,21 @@ func main() { // --- Central hub reporting --- var hubPusher *report.Pusher - if cfg.Hub.Enabled && cfg.Hub.URL != "" { - pushInterval, err := time.ParseDuration(cfg.Hub.PushInterval) - if err != nil { - pushInterval = 15 * time.Minute - } + if cfg.Hub.URL != "" && cfg.Hub.APIKey != "" { hubPusher = report.NewPusher(&cfg.Hub, logger) - sched.Every("hub-report", pushInterval, func(ctx context.Context) error { - r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths()) - return hubPusher.Push(r) - }) - logger.Printf("[INFO] Hub reporting enabled (every %s to %s)", pushInterval, cfg.Hub.URL) + if cfg.Hub.Enabled { + pushInterval, err := time.ParseDuration(cfg.Hub.PushInterval) + if err != nil { + pushInterval = 15 * time.Minute + } + sched.Every("hub-report", pushInterval, func(ctx context.Context) error { + r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths()) + return hubPusher.Push(r) + }) + logger.Printf("[INFO] Hub reporting enabled (every %s to %s)", pushInterval, cfg.Hub.URL) + } else { + logger.Printf("[INFO] Hub reporting disabled — will send disabled notification to %s", cfg.Hub.URL) + } } sched.Start(ctx) @@ -284,11 +288,27 @@ func main() { // Hub report if hubPusher != nil { - r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths()) - if err := hubPusher.Push(r); err != nil { - logger.Printf("[WARN] Startup hub report failed: %v", err) + if cfg.Hub.Enabled { + r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths()) + if err := hubPusher.Push(r); err != nil { + logger.Printf("[WARN] Startup hub report failed: %v", err) + } else { + logger.Println("[INFO] Startup hub report sent") + } } else { - logger.Println("[INFO] Startup hub report sent") + // Send a minimal "disabled" notification so hub knows reporting is intentionally off + r := &report.Report{ + Version: 1, + CustomerID: cfg.Customer.ID, + CustomerName: cfg.Customer.Name, + ControllerVersion: Version, + Timestamp: time.Now().UTC(), + ReportingDisabled: true, + Health: report.HealthReport{Status: "disabled", Issues: []string{}, Warnings: []string{}}, + Stacks: report.StacksReport{Deployed: []string{}, Available: []string{}}, + Containers: report.ContainerReport{List: []report.ContainerDetailReport{}}, + } + hubPusher.PushOnce(r) } } }() diff --git a/controller/internal/report/pusher.go b/controller/internal/report/pusher.go index 60b913d..2c69611 100644 --- a/controller/internal/report/pusher.go +++ b/controller/internal/report/pusher.go @@ -84,3 +84,39 @@ func (p *Pusher) Push(report *Report) error { p.logger.Printf("[WARN] Hub report push failed after 3 attempts: %v", lastErr) return nil } + +// PushOnce sends a single report regardless of the enabled flag. +// Used for one-time notifications (e.g., reporting-disabled on startup). +func (p *Pusher) PushOnce(report *Report) error { + if p.hubURL == "" || p.apiKey == "" { + return nil + } + + data, err := json.Marshal(report) + if err != nil { + p.logger.Printf("[WARN] Hub report marshal failed: %v", err) + return nil + } + + url := p.hubURL + "/api/v1/report" + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) + if err != nil { + return nil + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+p.apiKey) + + resp, err := p.httpClient.Do(req) + if err != nil { + p.logger.Printf("[WARN] Hub disabled-notification failed: %v", err) + return nil + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + p.logger.Printf("[INFO] Hub disabled-notification sent (%d bytes)", len(data)) + } + return nil +} diff --git a/controller/internal/report/types.go b/controller/internal/report/types.go index 463b884..5ee9185 100644 --- a/controller/internal/report/types.go +++ b/controller/internal/report/types.go @@ -9,6 +9,7 @@ type Report struct { CustomerName string `json:"customer_name"` ControllerVersion string `json:"controller_version"` Timestamp time.Time `json:"timestamp"` + ReportingDisabled bool `json:"reporting_disabled,omitempty"` System SystemReport `json:"system"` Storage []StorageReport `json:"storage"` Containers ContainerReport `json:"containers"`