# TASK: Fix hub report (v0.15.4) Three areas of work: 1. **Hub storage report** — already fixed in `builder.go`/`types.go` (applied before this task). 2. **Hub reporting lifecycle** — when hub reporting is disabled on a controller, the hub should know about it. Currently the hub just shows "DOWN" after reports stop arriving, with no distinction between "node crashed" vs "reporting turned off". The controller should send a one-time "disabled" notification so the hub can display the correct status. Also: hub report history should show dates (not just times), and hub should use storage labels. --- ## Fix 1: Hub storage report — include all storage paths (ALREADY DONE) The following changes have ALREADY been applied: - **`controller/internal/report/types.go`** — `Label` field added to `StorageReport` - **`controller/internal/report/builder.go`** — Storage section now iterates over all `storagePaths` using `system.GetDiskUsage()` These were applied before this task. No further changes needed in these files. --- ## Fix 2: Controller sends "disabled" notification when hub reporting is off ### Problem When `hub.enabled: false` but `hub.url` and `hub.api_key` ARE configured, the controller sends nothing to the hub. The hub has no way to know whether the node is dead or reporting was intentionally turned off. It just shows "DOWN" after the stale threshold (30min+). ### Design The intent behind the config values: - `hub.url` + `hub.api_key` configured → a relationship with the hub exists - `hub.enabled: false` → periodic reporting is turned off (but the hub relationship still exists) So: when `hub.enabled == false` but URL + API key are present, the controller should send **one** "reporting disabled" notification on startup. ### Controller changes #### Step 1: Add `ReportingDisabled` field to `Report` **File:** `controller/internal/report/types.go` Add a new field to the `Report` struct: ```go type Report struct { Version int `json:"version"` CustomerID string `json:"customer_id"` 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"` Backup BackupReport `json:"backup"` Health HealthReport `json:"health"` Stacks StacksReport `json:"stacks"` } ``` #### Step 2: Add `PushOnce()` method to Pusher **File:** `controller/internal/report/pusher.go` The existing `Push()` method checks `p.enabled` and silently returns if false. We need a method that pushes regardless of the `enabled` flag, for the one-time disabled notification. Add after `Push()`: ```go // 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 } ``` #### Step 3: Send disabled notification on startup **File:** `controller/cmd/controller/main.go` (~lines 248-261, 285-293) Change the hub reporting section. Currently: ```go // --- Central hub reporting --- var hubPusher *report.Pusher if cfg.Hub.Enabled && cfg.Hub.URL != "" { // ... create pusher, register scheduler ... } ``` Replace with: ```go // --- Central hub reporting --- var hubPusher *report.Pusher if cfg.Hub.URL != "" && cfg.Hub.APIKey != "" { hubPusher = report.NewPusher(&cfg.Hub, logger) 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) } } ``` Then in the startup goroutine (~line 285-293), change the hub report block: ```go // Hub report if hubPusher != nil { 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 { // Send a minimal "disabled" notification so hub knows we're alive but not reporting 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) } } ``` **Note:** The `Report`, `HealthReport`, `StacksReport`, `ContainerReport`, and `ContainerDetailReport` types are all in `controller/internal/report/types.go`. Import the `report` package (already imported as it's used on the line above). The minimal report includes empty slices for all list fields so JSON serialization produces `[]` not `null`. **Note:** `NewPusher` sets `enabled: cfg.Enabled`. When `cfg.Hub.Enabled == false`, `Push()` will silently return nil. The startup code uses `PushOnce()` instead for the disabled notification, and the scheduler is never registered, so the `enabled` flag doesn't matter. No change needed to `NewPusher`. --- ## Fix 4: Hub — handle "disabled" status + show dates in history ### Repo: `E:\git\felhom.eu` (hub code at `hub/`) ### 4a: Hub status logic — handle "disabled" **File:** `hub/internal/web/server.go` The `handleDashboard()` function (~line 142-151) determines `OverallStatus`: ```go if c.TimeSinceReport > time.Hour { dc.OverallStatus = "down" } else if c.TimeSinceReport > 30*time.Minute || c.HealthStatus == "warn" { dc.OverallStatus = "warn" } else if c.HealthStatus == "fail" { dc.OverallStatus = "down" } else { dc.OverallStatus = "ok" } ``` **Replace with:** ```go if c.HealthStatus == "disabled" { dc.OverallStatus = "disabled" } else if c.TimeSinceReport > time.Hour { dc.OverallStatus = "down" } else if c.TimeSinceReport > 30*time.Minute || c.HealthStatus == "warn" { dc.OverallStatus = "warn" } else if c.HealthStatus == "fail" { dc.OverallStatus = "down" } else { dc.OverallStatus = "ok" } ``` Same change in `handleCustomerDetail()` (~line 200-208): ```go overallStatus := "ok" if customer.HealthStatus == "disabled" { overallStatus = "disabled" } else if customer.TimeSinceReport > time.Hour { // ...existing logic... ``` Add "disabled" status color and icon to the existing functions: **In `statusColor()`:** ```go case "disabled": return "#94a3b8" // gray (same as default) ``` ### 4b: Dashboard template — show "PAUSED" badge **File:** `hub/internal/web/templates/dashboard.html` (line 46) **Current:** ```html {{if eq .OverallStatus "ok"}}OK{{else if eq .OverallStatus "warn"}}WARN{{else}}DOWN{{end}} ``` **Replace with:** ```html {{if eq .OverallStatus "ok"}}OK{{else if eq .OverallStatus "warn"}}WARN{{else if eq .OverallStatus "disabled"}}PAUSED{{else}}DOWN{{end}} ``` ### 4c: Customer detail — show "Reporting disabled" message **File:** `hub/internal/web/templates/customer.html` In the Health section (~line 130-155), add a check for disabled status BEFORE the existing `{{with .Report.health}}` block: After line 132 (`
Reporting has been disabled on this node
Enable it in the controller's controller.yaml: hub.enabled: true
Reporting has been disabled on this node
Enable it in the controller's controller.yaml: hub.enabled: true