From ba5d40615767e704e2c79f7bc9eaf14e78abb1cd Mon Sep 17 00:00:00 2001 From: GitCaddy Date: Wed, 14 Jan 2026 09:09:44 +0000 Subject: [PATCH] feat: Add runner health UI - unhealthy status display and CPU monitoring tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add unhealthy status display on runners list page (yellow color when online but unhealthy) - Add is_healthy field to RunnersStatusJSON API response - Add CPUInfo struct to RunnerCapability for CPU load monitoring - Add CPU tile to runner edit page showing load percentage and CPU count - Add real-time JavaScript polling for CPU tile updates - Add locale string for unhealthy status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- models/actions/runner.go | 11 ++++++- modules/structs/actions_capabilities.go | 10 ++++++ options/locale/locale_en-US.json | 1 + routers/web/shared/actions/runners.go | 11 +++++++ templates/shared/actions/runner_edit.tmpl | 39 ++++++++++++++++++++++- templates/shared/actions/runner_list.tmpl | 4 +-- 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/models/actions/runner.go b/models/actions/runner.go index b1bf8450dc..27ff09340c 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -118,8 +118,17 @@ func (r *ActionRunner) StatusName() string { return strings.ToLower(strings.TrimPrefix(r.Status().String(), "RUNNER_STATUS_")) } +// DisplayStatusName returns the status name for display, considering health status +// Returns "unhealthy" if the runner is online but has health issues +func (r *ActionRunner) DisplayStatusName() string { + if r.IsOnline() && !r.IsHealthy() { + return "unhealthy" + } + return r.StatusName() +} + func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { - return lang.TrString("actions.runners.status." + r.StatusName()) + return lang.TrString("actions.runners.status." + r.DisplayStatusName()) } func (r *ActionRunner) IsOnline() bool { diff --git a/modules/structs/actions_capabilities.go b/modules/structs/actions_capabilities.go index a2d965faec..730828c231 100644 --- a/modules/structs/actions_capabilities.go +++ b/modules/structs/actions_capabilities.go @@ -21,6 +21,15 @@ type DiskInfo struct { UsedPercent float64 `json:"used_percent"` } +// CPUInfo holds CPU load information for a runner +type CPUInfo struct { + NumCPU int `json:"num_cpu"` + LoadAvg1m float64 `json:"load_avg_1m"` + LoadAvg5m float64 `json:"load_avg_5m"` + LoadAvg15m float64 `json:"load_avg_15m"` + LoadPercent float64 `json:"load_percent"` +} + // DistroInfo holds Linux distribution information type DistroInfo struct { ID string `json:"id,omitempty"` // e.g., "ubuntu", "debian", "fedora" @@ -52,6 +61,7 @@ type RunnerCapability struct { Features *CapabilityFeatures `json:"features,omitempty"` Limitations []string `json:"limitations,omitempty"` Disk *DiskInfo `json:"disk,omitempty"` + CPU *CPUInfo `json:"cpu,omitempty"` Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"` SuggestedLabels []string `json:"suggested_labels,omitempty"` } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 722946a09a..47d34f8e18 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3702,6 +3702,7 @@ "actions.runners.status.idle": "Idle", "actions.runners.status.active": "Active", "actions.runners.status.offline": "Offline", + "actions.runners.status.unhealthy": "Unhealthy", "actions.runners.version": "Version", "actions.runners.reset_registration_token": "Reset registration token", "actions.runners.reset_registration_token_confirm": "Would you like to invalidate the current token and generate a new one?", diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index e24dd35513..bbe2b5b8b9 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -610,6 +610,7 @@ func RunnerStatusJSON(ctx *context.Context) { "id": runner.ID, "name": runner.Name, "is_online": runner.IsOnline(), + "is_healthy": runner.IsHealthy(), "status": runner.StatusLocaleName(ctx.Locale), "version": runner.Version, "labels": runner.AgentLabels, @@ -628,6 +629,15 @@ func RunnerStatusJSON(ctx *context.Context) { "used_percent": caps.Disk.UsedPercent, } } + if caps.CPU != nil { + response["cpu"] = map[string]any{ + "num_cpu": caps.CPU.NumCPU, + "load_avg_1m": caps.CPU.LoadAvg1m, + "load_avg_5m": caps.CPU.LoadAvg5m, + "load_avg_15m": caps.CPU.LoadAvg15m, + "load_percent": caps.CPU.LoadPercent, + } + } if caps.Bandwidth != nil { bw := map[string]any{ "download_mbps": caps.Bandwidth.DownloadMbps, @@ -676,6 +686,7 @@ func RunnersStatusJSON(ctx *context.Context) { item := map[string]any{ "id": runner.ID, "is_online": runner.IsOnline(), + "is_healthy": runner.IsHealthy(), "status": runner.StatusLocaleName(ctx.Locale), "version": runner.Version, } diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl index 220e17fa4d..9be1373cca 100644 --- a/templates/shared/actions/runner_edit.tmpl +++ b/templates/shared/actions/runner_edit.tmpl @@ -4,7 +4,7 @@
-
+
Status
@@ -37,6 +37,24 @@ {{end}}
+
+
+
CPU Load
+ {{if and .RunnerCapabilities .RunnerCapabilities.CPU}} + {{$cpuLoad := .RunnerCapabilities.CPU.LoadPercent}} + + {{if ge $cpuLoad 90.0}}{{svg "octicon-alert" 16}}{{else if ge $cpuLoad 70.0}}{{svg "octicon-alert" 16}}{{else}}{{svg "octicon-cpu" 16}}{{end}} + {{printf "%.0f" $cpuLoad}}% load + +
+ {{.RunnerCapabilities.CPU.NumCPU}} CPUs · {{printf "%.2f" .RunnerCapabilities.CPU.LoadAvg1m}} avg +
+ {{else}} + {{svg "octicon-cpu" 16}} No data +
Waiting for report
+ {{end}} +
+
Network
@@ -404,6 +422,25 @@ } } + // Update CPU tile - only change class and text + if (data.cpu) { + const cpuLabel = document.getElementById('cpu-label'); + const cpuText = document.getElementById('cpu-text'); + const cpuSubtext = document.getElementById('cpu-subtext'); + const loadPct = data.cpu.load_percent; + + if (cpuLabel) { + const color = loadPct >= 90 ? 'red' : (loadPct >= 70 ? 'yellow' : 'green'); + cpuLabel.className = 'ui ' + color + ' large label'; + } + if (cpuText) { + cpuText.textContent = Math.round(loadPct) + '% load'; + } + if (cpuSubtext) { + cpuSubtext.textContent = data.cpu.num_cpu + ' CPUs · ' + data.cpu.load_avg_1m.toFixed(2) + ' avg'; + } + } + // Update bandwidth tile - only change class and text if (data.bandwidth) { const bwLabel = document.getElementById('bw-label'); diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index 126a22a9f4..3f8c70cb7b 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -66,7 +66,7 @@ {{range .Runners}} - {{.StatusLocaleName ctx.Locale}} + {{.StatusLocaleName ctx.Locale}} {{.ID}}

{{.Name}}

{{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}} @@ -109,7 +109,7 @@ // Update status const statusEl = document.getElementById('status-' + runner.id); if (statusEl) { - statusEl.className = 'ui label ' + (runner.is_online ? 'green' : ''); + statusEl.className = 'ui label ' + (runner.is_online ? (runner.is_healthy ? 'green' : 'yellow') : ''); statusEl.textContent = runner.status; }