Adds a new page to view all waiting/blocked jobs for a specific runner label. This helps administrators identify which jobs are queued for particular runner labels and diagnose runner capacity issues.
212 lines
7.6 KiB
Handlebars
212 lines
7.6 KiB
Handlebars
<div class="runner-container">
|
|
|
|
<div id="queue-depth-container" class="ui segment" style="display: none;">
|
|
<h5 class="ui header">{{ctx.Locale.Tr "actions.runners.queue_depth"}}</h5>
|
|
<div id="queue-depth-content"></div>
|
|
<div id="stuck-alerts" class="tw-mt-2" style="display: none;"></div>
|
|
</div>
|
|
|
|
<h4 class="ui top attached header">
|
|
{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
|
|
<div class="ui right">
|
|
<div class="ui top right pointing dropdown jump">
|
|
<button class="ui primary tiny button">
|
|
{{ctx.Locale.Tr "actions.runners.new"}}
|
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
|
</button>
|
|
<div class="menu">
|
|
<div class="item">
|
|
<a href="https://git.marketally.com/gitcaddy/act_runner/src/branch/main/HOWTOSTART.md">{{ctx.Locale.Tr "actions.runners.new_notice"}}</a>
|
|
</div>
|
|
<div class="divider"></div>
|
|
<div class="header">
|
|
Registration Token
|
|
</div>
|
|
<div class="ui action input">
|
|
<input type="text" value="{{.RegistrationToken}}" readonly>
|
|
<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
|
|
{{svg "octicon-copy" 14}}
|
|
</button>
|
|
</div>
|
|
<div class="divider"></div>
|
|
<div class="item">
|
|
<a class="link-action" data-url="{{$.Link}}/reset_registration_token"
|
|
data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}"
|
|
>
|
|
{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</h4>
|
|
<div class="ui attached segment">
|
|
<form class="ui form ignore-dirty" id="user-list-search-form" action="{{$.Link}}">
|
|
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.runner_kind")}}
|
|
</form>
|
|
</div>
|
|
<div class="ui attached table segment">
|
|
<table class="ui very basic striped table unstackable">
|
|
<thead>
|
|
<tr>
|
|
<th data-sortt-asc="online" data-sortt-desc="offline">
|
|
{{ctx.Locale.Tr "actions.runners.status"}}
|
|
{{SortArrow "online" "offline" .SortType false}}
|
|
</th>
|
|
<th data-sortt-asc="newest" data-sortt-desc="oldest">
|
|
{{ctx.Locale.Tr "actions.runners.id"}}
|
|
{{SortArrow "oldest" "newest" .SortType false}}
|
|
</th>
|
|
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
|
|
{{ctx.Locale.Tr "actions.runners.name"}}
|
|
{{SortArrow "alphabetically" "reversealphabetically" .SortType false}}
|
|
</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.version"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.owner_type"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.labels"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.last_online"}}</th>
|
|
<th>{{ctx.Locale.Tr "actions.runners.current_task"}}</th>
|
|
<th>{{ctx.Locale.Tr "edit"}}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="runner-list-body">
|
|
{{range .Runners}}
|
|
<tr data-runner-id="{{.ID}}">
|
|
<td><span id="status-{{.ID}}" class="ui label {{if .IsOnline}}{{if .IsHealthy}}green{{else}}yellow{{end}}{{end}}">{{.StatusLocaleName ctx.Locale}}</span></td>
|
|
<td>{{.ID}}</td>
|
|
<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>
|
|
<td id="version-{{.ID}}">{{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}}</td>
|
|
<td><span data-tooltip-content="{{.BelongsToOwnerName}}">{{.BelongsToOwnerType.LocaleString ctx.Locale}}</span></td>
|
|
<td>
|
|
<span class="flex-text-inline">{{range .AgentLabels}}<span class="ui label">{{.}}</span>{{end}}</span>
|
|
</td>
|
|
<td id="lastonline-{{.ID}}">{{if .LastOnline}}{{DateUtils.TimeSince .LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</td>
|
|
<td id="current-task-{{.ID}}" class="tw-text-sm">
|
|
<span class="tw-text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
{{if .EditableInContext $.RunnerOwnerID $.RunnerRepoID}}
|
|
<a href="{{$.Link}}/{{.ID}}">{{svg "octicon-pencil"}}</a>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{else}}
|
|
<tr>
|
|
<td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "actions.runners.none"}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{{template "base/paginate" .}}
|
|
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const statusUrl = '{{$.Link}}/status';
|
|
const queueDepthUrl = '{{$.Link}}/queue-depth';
|
|
const pollInterval = 30000; // 30 seconds
|
|
|
|
function updateQueueDepth() {
|
|
fetch(queueDepthUrl, {
|
|
headers: {'Accept': 'application/json'}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const container = document.getElementById('queue-depth-container');
|
|
const content = document.getElementById('queue-depth-content');
|
|
const alerts = document.getElementById('stuck-alerts');
|
|
|
|
if (!data || data.length === 0) {
|
|
container.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
container.style.display = 'block';
|
|
|
|
let html = '<div class="tw-flex tw-flex-wrap tw-gap-2">';
|
|
let stuckHtml = '';
|
|
let hasStuck = false;
|
|
|
|
for (const item of data) {
|
|
const labelClass = item.stuck_jobs > 0 ? 'red' : (item.job_count > 5 ? 'yellow' : 'blue');
|
|
let waitInfo = item.oldest_wait ? ' (oldest: ' + item.oldest_wait + ')' : '';
|
|
const queueUrl = '{{$.Link}}/queue?label=' + encodeURIComponent(item.label);
|
|
html += '<a href="' + queueUrl + '" class="ui ' + labelClass + ' label" title="' + item.job_count + ' jobs waiting' + waitInfo + ' - Click to view">' +
|
|
item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + '</a>';
|
|
|
|
if (item.stuck_jobs > 0) {
|
|
hasStuck = true;
|
|
stuckHtml += '<div class="ui warning message tw-py-2 tw-px-3"><i class="exclamation triangle icon"></i>' +
|
|
item.label + ': ' + item.stuck_jobs + ' job(s) waiting > 30 min</div>';
|
|
}
|
|
}
|
|
html += '</div>';
|
|
content.innerHTML = html;
|
|
|
|
if (hasStuck) {
|
|
alerts.style.display = 'block';
|
|
alerts.innerHTML = stuckHtml;
|
|
} else {
|
|
alerts.style.display = 'none';
|
|
}
|
|
})
|
|
.catch(err => console.error('Failed to update queue depth:', err));
|
|
}
|
|
|
|
function updateRunners() {
|
|
fetch(statusUrl, {
|
|
headers: {'Accept': 'application/json'}
|
|
})
|
|
.then(response => response.json())
|
|
.then(runners => {
|
|
runners.forEach(runner => {
|
|
// Update status
|
|
const statusEl = document.getElementById('status-' + runner.id);
|
|
if (statusEl) {
|
|
statusEl.className = 'ui label ' + (runner.is_online ? (runner.is_healthy ? 'green' : 'yellow') : '');
|
|
statusEl.textContent = runner.status;
|
|
}
|
|
|
|
// Update version
|
|
const versionEl = document.getElementById('version-' + runner.id);
|
|
if (versionEl && runner.version) {
|
|
versionEl.textContent = runner.version;
|
|
}
|
|
|
|
// Update last online
|
|
const lastOnlineEl = document.getElementById('lastonline-' + runner.id);
|
|
if (lastOnlineEl && runner.last_online_relative) {
|
|
lastOnlineEl.textContent = runner.last_online_relative;
|
|
}
|
|
|
|
// Update current task
|
|
const taskEl = document.getElementById('current-task-' + runner.id);
|
|
if (taskEl) {
|
|
if (runner.current_task) {
|
|
const task = runner.current_task;
|
|
taskEl.innerHTML = '<span class="ui small blue label">' +
|
|
'<i class="spinner loading icon"></i> ' + task.duration +
|
|
'</span>';
|
|
} else {
|
|
taskEl.innerHTML = '<span class="tw-text-muted">-</span>';
|
|
}
|
|
}
|
|
});
|
|
})
|
|
.catch(err => console.error('Failed to update runner status:', err));
|
|
}
|
|
|
|
// Poll every 30 seconds
|
|
setInterval(updateRunners, pollInterval);
|
|
setInterval(updateQueueDepth, pollInterval);
|
|
|
|
// Also update immediately on page load after a short delay
|
|
setTimeout(updateRunners, 2000);
|
|
updateQueueDepth();
|
|
})();
|
|
</script>
|