2
0
Files
gitcaddy-server/templates/shared/actions/runner_list.tmpl
logikonline 73ab59f158 feat(actions): add waiting jobs view filtered by runner label
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.
2026-01-25 11:27:07 -05:00

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>