Files
deploy-felhom-compose/TASK.md
T
2026-02-19 09:39:29 +01:00

16 KiB

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.goLabel 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:

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():

// 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:

// --- Central hub reporting ---
var hubPusher *report.Pusher
if cfg.Hub.Enabled && cfg.Hub.URL != "" {
    // ... create pusher, register scheduler ...
}

Replace with:

// --- 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:

// 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:

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:

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):

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():

case "disabled":
    return "#94a3b8" // gray (same as default)

4b: Dashboard template — show "PAUSED" badge

File: hub/internal/web/templates/dashboard.html (line 46)

Current:

{{if eq .OverallStatus "ok"}}OK{{else if eq .OverallStatus "warn"}}WARN{{else}}DOWN{{end}}

Replace with:

{{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 (<h2>Health</h2>), add:

{{if eq .OverallStatus "disabled"}}
<p class="health-status health-status-disabled">Reporting has been disabled on this node</p>
<p class="hint">Enable it in the controller's <code>controller.yaml</code>: <code>hub.enabled: true</code></p>
{{else}}

And after the existing {{end}} that closes {{with .Report.health}} (line 155), add:

{{end}}

So the full Health section becomes:

<section class="card">
    <h2>Health</h2>
    {{if eq .OverallStatus "disabled"}}
    <p class="health-status health-status-disabled">Reporting has been disabled on this node</p>
    <p class="hint">Enable it in the controller's <code>controller.yaml</code>: <code>hub.enabled: true</code></p>
    {{else}}
    {{with .Report.health}}
    ... existing content ...
    {{end}}
    {{end}}
</section>

4d: Storage labels — use label with fallback to mount

File: hub/internal/web/templates/customer.html (line 65)

Current:

<span class="metric-label">{{index . "mount"}}</span>

Replace with:

<span class="metric-label">{{with index . "label"}}{{.}}{{else}}{{index . "mount"}}{{end}}</span>

4e: Report history — show date + time

File: hub/internal/web/templates/customer.html (line 216)

Current:

<td>{{.ReceivedAt.Format "15:04:05"}}</td>

Replace with:

<td>{{.ReceivedAt.Format "Jan 02 15:04"}}</td>

This shows "Feb 18 17:10" instead of just "17:10:54", making it clear when reports are from different days.

4f: CSS for disabled status badge

File: hub/internal/web/templates/style.css

Search for .status-badge- CSS rules. Add a rule for disabled:

.status-badge-disabled {
    background: #475569;
    color: #e2e8f0;
}

Use a neutral gray — not red (DOWN) or yellow (WARN). This visually signals "intentionally paused", not "something is wrong".


Summary of all changes

Controller repo (deploy-felhom-compose)

File Change
controller/internal/report/types.go Add ReportingDisabled bool field to Report struct (Label already applied)
controller/internal/report/pusher.go Add PushOnce() method
controller/cmd/controller/main.go Always create Pusher when URL+key present; send disabled notification when hub.enabled == false

Hub repo (felhom.eu)

File Change
hub/internal/web/server.go Handle "disabled" in status logic (dashboard + detail handlers), add disabled color
hub/internal/web/templates/dashboard.html Show "PAUSED" badge for disabled status
hub/internal/web/templates/customer.html Disabled health message, storage labels, date+time in history
hub/internal/web/templates/style.css Add .status-badge-disabled CSS

Runtime config (demo node)

File Change
/opt/docker/felhom-controller/controller.yaml Set hub.enabled: true

Build & Deploy

Part A: Controller (v0.15.4)

SSH=/c/Windows/System32/OpenSSH/ssh.exe
# 1. Commit & push
cd e:/git/deploy-felhom-compose
git add -A && git commit -m "v0.15.4: Show all storage paths on dashboard/monitoring, hub disabled notification" && git push
# 2. Build
$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-controller && git -C ~/git/deploy-felhom-compose pull && ./build.sh v0.15.4 --push"
# 3. Enable hub reporting + deploy
$SSH kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && sudo sed -i 's/enabled: false/enabled: true/' controller.yaml && sudo docker pull gitea.dooplex.hu/admin/felhom-controller:v0.15.4 && sudo sed -i 's|image: gitea.dooplex.hu/admin/felhom-controller:.*|image: gitea.dooplex.hu/admin/felhom-controller:v0.15.4|' docker-compose.yml && sudo docker compose up -d"
# 4. Verify
$SSH kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
# 5. Check hub reporting is active
$SSH kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 5 2>&1 | grep -i hub"

Part B: Hub (v0.1.6)

# 1. Commit & push
cd e:/git/felhom.eu
git add -A && git commit -m "hub v0.1.6: Handle disabled reporting status, storage labels, date in history" && git push
# 2. Build + push
$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-hub && ./build.sh 0.1.6 --push"
# 3. Deploy to k3s
$SSH kisfenyo@192.168.0.180 "sudo kubectl set image -n felhom-system deploy/hub hub=gitea.dooplex.hu/admin/felhom-hub:0.1.6"
# 4. Verify
$SSH kisfenyo@192.168.0.180 "sudo kubectl get pods -n felhom-system -l app=hub && echo '---' && sudo kubectl logs -n felhom-system -l app=hub --tail 10"

Compile check

Always run go build ./... in controller/ before committing to ensure no compile errors.

Documentation

Add a CHANGELOG.md entry at the top (under ## Changelog). Read the first 30 lines to see the format, then insert a new entry. Example:

### vX.X.X (2026-02-19 session XX)
- **v0.15.4 — Show all storage paths on dashboard + hub reporting improvements:**

  Dashboard ("Vezérlőpult") and monitoring ("Rendszermonitor") pages now show usage bars for ALL registered storage paths instead of just one hardcoded "Külső HDD" bar. Each bar displays the storage label and usage from settings.

  Hub storage report now correctly includes all registered storage paths with proper mount paths and labels. Previously it sent only root `/` and one HDD entry with an empty mount path (used deprecated `cfg.Paths.HDDPath`).

  When hub reporting is disabled (`hub.enabled: false`) but hub URL/key are configured, the controller now sends a one-time "disabled" notification so the hub shows "PAUSED" instead of "DOWN".

  **Files modified:** `internal/web/handlers.go`, `internal/web/templates/dashboard.html`, `internal/web/templates/monitoring.html`, `internal/report/types.go`, `internal/report/pusher.go`, `cmd/controller/main.go`
  **Hub files:** `hub/internal/web/server.go`, `hub/internal/web/templates/dashboard.html`, `hub/internal/web/templates/customer.html`, `hub/internal/web/templates/style.css`

Update version in C:\Users\User\.claude\projects\e--git\memory\MEMORY.md to v0.15.4.

Verification

After deploying v0.15.4 (controller) and v0.1.6 (hub):

  1. Wait ~30s for startup hub report, then check hub.felhom.eu/customers/demo-felhom:
    • Health section should show STATUS: OK with no warnings (since hub.enabled is now true)
    • Storage section should show three bars with labels: "SSD", "USB HDD 1TB", "SYS Storage 350G"
    • Report history should show dates (e.g., "Feb 19 09:46")
  2. To test PAUSED state: set hub.enabled: false on demo node, restart controller. Hub should show "PAUSED" (gray badge), not "DOWN" (red)