Some checks failed
Build and Release / Lint (push) Failing after 2m13s
Build and Release / Unit Tests (push) Successful in 2m48s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m0s
- Add /api/v2/repos/{owner}/{repo}/actions/runners/status endpoint
- Add /api/v2/repos/{owner}/{repo}/actions/runners/{id}/status endpoint
- Add internal status JSON endpoint for admin panel polling
- Add JavaScript polling (10s interval) to runner edit page
- Status tiles now auto-update: online/offline, disk, bandwidth
🤖 Generated with Claude Code
382 lines
17 KiB
Handlebars
382 lines
17 KiB
Handlebars
<div class="runner-container">
|
|
<h4 class="ui top attached header">
|
|
{{ctx.Locale.Tr "actions.runners.runner_title"}} {{.Runner.ID}} {{.Runner.Name}}
|
|
</h4>
|
|
<div class="ui attached segment">
|
|
<!-- Health Status Tiles -->
|
|
<div class="ui three column stackable grid tw-mb-4">
|
|
<div class="column">
|
|
<div class="ui segment tw-text-center">
|
|
<div class="tw-text-sm tw-mb-1" style="opacity: 0.8;">Status</div>
|
|
<span class="ui {{if .Runner.IsOnline}}green{{else}}red{{end}} large label">
|
|
{{if .Runner.IsOnline}}{{svg "octicon-check-circle" 16}}{{else}}{{svg "octicon-x-circle" 16}}{{end}}
|
|
{{.Runner.StatusLocaleName ctx.Locale}}
|
|
</span>
|
|
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
|
|
{{if .Runner.IsOnline}}Connected{{else if .Runner.LastOnline}}Last seen {{DateUtils.TimeSince .Runner.LastOnline}}{{else}}Never connected{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="column">
|
|
<div class="ui segment tw-text-center">
|
|
<div class="tw-text-sm tw-mb-1" style="opacity: 0.8;">Disk Space</div>
|
|
{{if and .RunnerCapabilities .RunnerCapabilities.Disk}}
|
|
{{$diskUsed := .RunnerCapabilities.Disk.UsedPercent}}
|
|
{{$diskFreeGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Free) 1073741824.0}}
|
|
{{$diskTotalGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Total) 1073741824.0}}
|
|
<span class="ui {{if ge $diskUsed 95.0}}red{{else if ge $diskUsed 85.0}}yellow{{else}}green{{end}} large label">
|
|
{{if ge $diskUsed 95.0}}{{svg "octicon-alert" 16}}{{else if ge $diskUsed 85.0}}{{svg "octicon-alert" 16}}{{else}}{{svg "octicon-database" 16}}{{end}}
|
|
{{printf "%.0f" $diskUsed}}% used
|
|
</span>
|
|
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
|
|
{{printf "%.1f" $diskFreeGB}} GB free of {{printf "%.0f" $diskTotalGB}} GB
|
|
</div>
|
|
{{else}}
|
|
<span class="ui grey large label">{{svg "octicon-database" 16}} No data</span>
|
|
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">Waiting for report</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
<div class="column">
|
|
<div class="ui segment tw-text-center">
|
|
<div class="tw-text-sm tw-mb-1" style="opacity: 0.8;">Network</div>
|
|
{{if and .RunnerCapabilities .RunnerCapabilities.Bandwidth}}
|
|
<span class="ui {{if ge .RunnerCapabilities.Bandwidth.DownloadMbps 100.0}}green{{else if ge .RunnerCapabilities.Bandwidth.DownloadMbps 10.0}}blue{{else}}yellow{{end}} large label">
|
|
{{svg "octicon-arrow-down" 16}} {{printf "%.0f" .RunnerCapabilities.Bandwidth.DownloadMbps}} Mbps
|
|
</span>
|
|
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
|
|
{{if gt .RunnerCapabilities.Bandwidth.Latency 0.0}}{{printf "%.0f" .RunnerCapabilities.Bandwidth.Latency}} ms latency{{end}}
|
|
{{if .RunnerCapabilities.Bandwidth.TestedAt}}- tested {{DateUtils.TimeSince .RunnerCapabilities.Bandwidth.TestedAt}}{{end}}
|
|
</div>
|
|
{{else}}
|
|
<span class="ui grey large label">{{svg "octicon-globe" 16}} No data</span>
|
|
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">Waiting for test</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ui two column stackable grid">
|
|
<!-- Left Column: Runner Info & Controls -->
|
|
<div class="column">
|
|
<div class="ui segment">
|
|
<h5 class="ui header">Runner Information</h5>
|
|
<table class="ui very basic table">
|
|
<tbody>
|
|
<tr>
|
|
<td style="width: 100px; opacity: 0.8;">Version</td>
|
|
<td><span class="ui small blue label">{{.Runner.Version}}</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="opacity: 0.8;">Owner</td>
|
|
<td data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="opacity: 0.8;">Labels</td>
|
|
<td>
|
|
{{range .Runner.AgentLabels}}
|
|
<form method="post" action="{{$.Link}}/remove-label" style="display:inline;">
|
|
{{$.CsrfTokenHtml}}
|
|
<input type="hidden" name="label" value="{{.}}">
|
|
<button type="submit" class="ui small blue label tw-my-1" style="cursor:pointer;">{{.}} {{svg "octicon-x" 12}}</button>
|
|
</form>
|
|
{{end}}
|
|
{{if not .Runner.AgentLabels}}<span style="opacity: 0.6;">No labels</span>{{end}}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Suggested Labels Section -->
|
|
{{if .RunnerCapabilities}}
|
|
<div class="ui segment">
|
|
<h5 class="ui header">{{svg "octicon-light-bulb" 16}} Suggested Labels</h5>
|
|
<p class="tw-text-sm tw-mb-2" style="opacity: 0.7;">Based on detected capabilities. Click + to add individually.</p>
|
|
<div class="tw-flex tw-flex-wrap tw-gap-2" id="suggested-labels">
|
|
{{$labels := .Runner.AgentLabels}}
|
|
{{if eq .RunnerCapabilities.OS "linux"}}
|
|
{{if not (SliceUtils.Contains $labels "linux")}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="linux"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} linux</button></form>
|
|
{{else}}<span class="ui small teal label">linux</span>{{end}}
|
|
{{if not (SliceUtils.Contains $labels "linux-latest")}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="linux-latest"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} linux-latest</button></form>
|
|
{{else}}<span class="ui small teal label">linux-latest</span>{{end}}
|
|
{{else if eq .RunnerCapabilities.OS "windows"}}
|
|
{{if not (SliceUtils.Contains $labels "windows")}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="windows"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} windows</button></form>
|
|
{{else}}<span class="ui small teal label">windows</span>{{end}}
|
|
{{if not (SliceUtils.Contains $labels "windows-latest")}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="windows-latest"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} windows-latest</button></form>
|
|
{{else}}<span class="ui small teal label">windows-latest</span>{{end}}
|
|
{{else if eq .RunnerCapabilities.OS "darwin"}}
|
|
{{if not (SliceUtils.Contains $labels "macos")}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="macos"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} macos</button></form>
|
|
{{else}}<span class="ui small teal label">macos</span>{{end}}
|
|
{{if not (SliceUtils.Contains $labels "macos-latest")}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="macos-latest"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} macos-latest</button></form>
|
|
{{else}}<span class="ui small teal label">macos-latest</span>{{end}}
|
|
{{end}}
|
|
{{if and .RunnerCapabilities.Distro .RunnerCapabilities.Distro.ID}}
|
|
{{$distro := .RunnerCapabilities.Distro.ID}}
|
|
{{$distroLatest := printf "%s-latest" .RunnerCapabilities.Distro.ID}}
|
|
{{if not (SliceUtils.Contains $labels $distro)}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="{{$distro}}"><button type="submit" class="ui small purple label" style="cursor:pointer;">{{svg "octicon-plus" 12}} {{$distro}}</button></form>
|
|
{{else}}<span class="ui small purple label">{{$distro}}</span>{{end}}
|
|
{{if not (SliceUtils.Contains $labels $distroLatest)}}
|
|
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="{{$distroLatest}}"><button type="submit" class="ui small purple label" style="cursor:pointer;">{{svg "octicon-plus" 12}} {{$distroLatest}}</button></form>
|
|
{{else}}<span class="ui small purple label">{{$distroLatest}}</span>{{end}}
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<form class="ui form" method="post">
|
|
{{template "base/disable_form_autofill"}}
|
|
<div class="ui segment">
|
|
<h5 class="ui header">AI Instructions</h5>
|
|
<p class="tw-text-sm tw-mb-2" style="opacity: 0.7;">Additional context for AI when selecting this runner for jobs.</p>
|
|
<div class="field">
|
|
<textarea id="description" name="description" rows="3" placeholder="e.g., Use for heavy builds, has GPU, limited to 2 concurrent jobs...">{{.Runner.Description}}</textarea>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Right Column: Capabilities -->
|
|
<div class="column">
|
|
{{if .Runner.CapabilitiesJSON}}
|
|
<div class="ui segment runner-capabilities">
|
|
<h5 class="ui header">{{ctx.Locale.Tr "actions.runners.capabilities"}}</h5>
|
|
{{if .RunnerCapabilities}}
|
|
{{if .RunnerCapabilities.OS}}
|
|
<div class="field tw-mb-3">
|
|
<label>{{ctx.Locale.Tr "actions.runners.capabilities.os"}}</label>
|
|
<span class="ui small blue label">{{.RunnerCapabilities.OS}}/{{.RunnerCapabilities.Arch}}</span>
|
|
{{if and .RunnerCapabilities.Distro .RunnerCapabilities.Distro.PrettyName}}
|
|
<span class="ui small label">{{.RunnerCapabilities.Distro.PrettyName}}</span>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="field tw-mb-3">
|
|
<label>{{ctx.Locale.Tr "actions.runners.capabilities.docker"}}</label>
|
|
{{if .RunnerCapabilities.Docker}}
|
|
<span class="ui small green label">{{svg "octicon-check" 14}} Available</span>
|
|
{{else}}
|
|
<span class="ui small orange label">{{svg "octicon-x" 14}} Not available</span>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{if .RunnerCapabilities.Shell}}
|
|
<div class="field tw-mb-3">
|
|
<label>{{ctx.Locale.Tr "actions.runners.capabilities.shells"}}</label>
|
|
<div>
|
|
{{range .RunnerCapabilities.Shell}}
|
|
<span class="ui small teal label tw-mr-1">{{.}}</span>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .RunnerCapabilities.Tools}}
|
|
<div class="field tw-mb-3">
|
|
<label>{{ctx.Locale.Tr "actions.runners.capabilities.tools"}}</label>
|
|
<div class="tw-flex tw-flex-wrap tw-gap-1">
|
|
{{range $tool, $versions := .RunnerCapabilities.Tools}}
|
|
<span class="ui small purple label">{{$tool}} {{range $versions}}{{.}} {{end}}</span>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .RunnerCapabilities.Limitations}}
|
|
<div class="field tw-mb-3">
|
|
<label>{{ctx.Locale.Tr "actions.runners.capabilities.limitations"}}</label>
|
|
<ul class="tw-mt-1 tw-ml-4 tw-text-sm">
|
|
{{range .RunnerCapabilities.Limitations}}
|
|
<li>{{.}}</li>
|
|
{{end}}
|
|
</ul>
|
|
</div>
|
|
{{end}}
|
|
{{else}}
|
|
<pre class="tw-text-sm"><code>{{.Runner.CapabilitiesJSON}}</code></pre>
|
|
{{end}}
|
|
</div>
|
|
{{else}}
|
|
<div class="ui segment">
|
|
<h5 class="ui header">{{ctx.Locale.Tr "actions.runners.capabilities"}}</h5>
|
|
<p style="opacity: 0.7;">No capabilities reported</p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons - Full Width -->
|
|
<div class="tw-flex tw-gap-2 tw-flex-wrap tw-mt-4">
|
|
<button class="ui primary button" form="runner-form" data-url="{{.Link}}">
|
|
{{svg "octicon-check" 14}} Update Instructions
|
|
</button>
|
|
{{if .RunnerCapabilities}}
|
|
<button class="ui teal button" type="button" onclick="document.getElementById('suggested-labels-form').submit()">
|
|
{{svg "octicon-light-bulb" 14}} Use All Suggested Labels
|
|
</button>
|
|
{{end}}
|
|
<button class="ui secondary button" type="button" onclick="document.getElementById('bandwidth-form').submit()">
|
|
{{svg "octicon-sync" 14}} Check Bandwidth
|
|
</button>
|
|
<button class="ui red button delete-button" data-url="{{.Link}}/delete" data-modal="#runner-delete-modal" type="button">
|
|
{{svg "octicon-trash" 14}} Delete
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Hidden Forms -->
|
|
<form id="bandwidth-form" method="post" action="{{.Link}}/bandwidth-test" style="display:none">
|
|
{{.CsrfTokenHtml}}
|
|
</form>
|
|
<form id="suggested-labels-form" method="post" action="{{.Link}}/use-suggested-labels" style="display:none">
|
|
{{.CsrfTokenHtml}}
|
|
</form>
|
|
</div>
|
|
|
|
<h4 class="ui top attached header">
|
|
{{ctx.Locale.Tr "actions.runners.task_list"}}
|
|
</h4>
|
|
<div class="ui attached segment">
|
|
<table class="ui very basic striped table unstackable">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.task_list.status"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.task_list.repository"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.task_list.commit"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.task_list.done_at"}}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Tasks}}
|
|
<tr>
|
|
<td><a href="{{.GetRunLink}}" target="_blank">{{.ID}}</a></td>
|
|
<td><span class="ui label task-status-{{.Status.String}}">{{.Status.LocaleString ctx.Locale}}</span></td>
|
|
<td><a href="{{.GetRepoLink}}" target="_blank">{{.GetRepoName}}</a></td>
|
|
<td>
|
|
<strong><a href="{{.GetCommitLink}}" target="_blank">{{ShortSha .CommitSHA}}</a></strong>
|
|
</td>
|
|
<td>{{if .IsStopped}}
|
|
<span>{{DateUtils.TimeSince .Stopped}}</span>
|
|
{{else}}-{{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
{{if not .Tasks}}
|
|
<tr>
|
|
<td colspan="5">{{ctx.Locale.Tr "actions.runners.task_list.no_tasks"}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{template "base/paginate" .}}
|
|
</div>
|
|
<div class="ui g-modal-confirm delete modal" id="runner-delete-modal">
|
|
<div class="header">
|
|
{{svg "octicon-trash"}}
|
|
{{ctx.Locale.Tr "actions.runners.delete_runner_header"}}
|
|
</div>
|
|
<div class="content">
|
|
<p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice"}}</p>
|
|
</div>
|
|
{{template "base/modal_actions_confirm" .}}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const statusUrl = '{{.Link}}/status';
|
|
const pollInterval = 10000; // 10 seconds
|
|
|
|
function formatBytes(bytes) {
|
|
const gb = bytes / 1073741824;
|
|
return gb.toFixed(1) + ' GB';
|
|
}
|
|
|
|
function updateStatus() {
|
|
fetch(statusUrl, {
|
|
headers: {'Accept': 'application/json'}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Update status tile
|
|
const statusTile = document.querySelector('.runner-container .column:first-child .segment');
|
|
if (statusTile) {
|
|
const statusLabel = statusTile.querySelector('.label');
|
|
const statusText = statusTile.querySelector('.tw-text-xs');
|
|
|
|
if (statusLabel) {
|
|
statusLabel.className = 'ui ' + (data.is_online ? 'green' : 'red') + ' large label';
|
|
statusLabel.innerHTML = (data.is_online ?
|
|
'<svg class="svg octicon-check-circle" width="16" height="16"><use xlink:href="#octicon-check-circle"></use></svg>' :
|
|
'<svg class="svg octicon-x-circle" width="16" height="16"><use xlink:href="#octicon-x-circle"></use></svg>') +
|
|
' ' + data.status;
|
|
}
|
|
|
|
if (statusText) {
|
|
statusText.textContent = data.is_online ? 'Connected' :
|
|
(data.last_online ? 'Last seen ' + new Date(data.last_online).toLocaleString() : 'Never connected');
|
|
}
|
|
}
|
|
|
|
// Update disk tile
|
|
if (data.disk) {
|
|
const diskTile = document.querySelector('.runner-container .column:nth-child(2) .segment');
|
|
if (diskTile) {
|
|
const diskLabel = diskTile.querySelector('.label');
|
|
const diskText = diskTile.querySelector('.tw-text-xs');
|
|
const usedPct = data.disk.used_percent;
|
|
|
|
if (diskLabel) {
|
|
const color = usedPct >= 95 ? 'red' : (usedPct >= 85 ? 'yellow' : 'green');
|
|
const icon = usedPct >= 85 ? 'octicon-alert' : 'octicon-database';
|
|
diskLabel.className = 'ui ' + color + ' large label';
|
|
diskLabel.innerHTML = '<svg class="svg ' + icon + '" width="16" height="16"><use xlink:href="#' + icon + '"></use></svg> ' +
|
|
Math.round(usedPct) + '% used';
|
|
}
|
|
|
|
if (diskText) {
|
|
diskText.textContent = formatBytes(data.disk.free_bytes) + ' free of ' + formatBytes(data.disk.total_bytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update bandwidth tile
|
|
if (data.bandwidth) {
|
|
const bwTile = document.querySelector('.runner-container .column:nth-child(3) .segment');
|
|
if (bwTile) {
|
|
const bwLabel = bwTile.querySelector('.label');
|
|
const bwText = bwTile.querySelector('.tw-text-xs');
|
|
const mbps = data.bandwidth.download_mbps;
|
|
|
|
if (bwLabel) {
|
|
const color = mbps >= 100 ? 'green' : (mbps >= 10 ? 'blue' : 'yellow');
|
|
bwLabel.className = 'ui ' + color + ' large label';
|
|
bwLabel.innerHTML = '<svg class="svg octicon-arrow-down" width="16" height="16"><use xlink:href="#octicon-arrow-down"></use></svg> ' +
|
|
Math.round(mbps) + ' Mbps';
|
|
}
|
|
|
|
if (bwText && data.bandwidth.latency_ms) {
|
|
let text = Math.round(data.bandwidth.latency_ms) + ' ms latency';
|
|
if (data.bandwidth.tested_at) {
|
|
text += ' - tested ' + new Date(data.bandwidth.tested_at).toLocaleString();
|
|
}
|
|
bwText.textContent = text;
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.catch(err => console.log('Status poll error:', err));
|
|
}
|
|
|
|
// Start polling
|
|
setInterval(updateStatus, pollInterval);
|
|
})();
|
|
</script>
|