2
0

feat(runners): add AJAX polling for real-time status updates
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m43s
Build and Release / Unit Tests (push) Successful in 2m1s
Build and Release / Lint (push) Failing after 2m5s
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

- Add element IDs to status/disk/bandwidth tiles for targeted updates
- Add JavaScript polling every 10 seconds to update runner status
- Preserve SVG icons during AJAX updates by separating icon and text spans
- Add form ID to runner-form for Update Instructions button
- Show Connected when online, Last seen when offline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
GitCaddy
2026-01-11 18:01:53 +00:00
parent 15bd1d61c4
commit 3bbd048204

View File

@@ -8,11 +8,11 @@
<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 id="status-label" class="ui {{if .Runner.IsOnline}}green{{else}}red{{end}} large label">
<span id="status-icon">{{if .Runner.IsOnline}}{{svg "octicon-check-circle" 16}}{{else}}{{svg "octicon-x-circle" 16}}{{end}}</span>
<span id="status-text">{{.Runner.StatusLocaleName ctx.Locale}}</span>
</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
<div id="status-subtext" 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>
@@ -24,16 +24,16 @@
{{$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 id="disk-label" class="ui {{if ge $diskUsed 95.0}}red{{else if ge $diskUsed 85.0}}yellow{{else}}green{{end}} large label">
<span id="disk-icon">{{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}}</span>
<span id="disk-text">{{printf "%.0f" $diskUsed}}% used</span>
</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
<div id="disk-subtext" 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>
<span id="disk-label" class="ui grey large label"><span id="disk-icon">{{svg "octicon-database" 16}}</span> <span id="disk-text">No data</span></span>
<div id="disk-subtext" class="tw-text-xs tw-mt-2" style="opacity: 0.7;">Waiting for report</div>
{{end}}
</div>
</div>
@@ -41,16 +41,17 @@
<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 id="bw-label" class="ui {{if ge .RunnerCapabilities.Bandwidth.DownloadMbps 100.0}}green{{else if ge .RunnerCapabilities.Bandwidth.DownloadMbps 10.0}}blue{{else}}yellow{{end}} large label">
<span id="bw-icon">{{svg "octicon-arrow-down" 16}}</span>
<span id="bw-text">{{printf "%.0f" .RunnerCapabilities.Bandwidth.DownloadMbps}} Mbps</span>
</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
<div id="bw-subtext" 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>
<span id="bw-label" class="ui grey large label"><span id="bw-icon">{{svg "octicon-globe" 16}}</span> <span id="bw-text">No data</span></span>
<div id="bw-subtext" class="tw-text-xs tw-mt-2" style="opacity: 0.7;">Waiting for test</div>
{{end}}
</div>
</div>
@@ -131,7 +132,7 @@
</div>
{{end}}
<form class="ui form" method="post">
<form id="runner-form" class="ui form" method="post">
{{template "base/disable_form_autofill"}}
<div class="ui segment">
<h5 class="ui header">AI Instructions</h5>
@@ -305,70 +306,66 @@
})
.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');
// Update status tile - only change class and text, preserve icons
const statusLabel = document.getElementById('status-label');
const statusText = document.getElementById('status-text');
const statusSubtext = document.getElementById('status-subtext');
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');
if (statusLabel) {
statusLabel.className = 'ui ' + (data.is_online ? 'green' : 'red') + ' large label';
}
if (statusText) {
statusText.textContent = data.status;
}
if (statusSubtext) {
if (data.is_online) {
statusSubtext.textContent = 'Connected';
} else if (data.last_online) {
statusSubtext.textContent = 'Last seen ' + new Date(data.last_online).toLocaleString();
} else {
statusSubtext.textContent = 'Never connected';
}
}
// Update disk tile
// Update disk tile - only change class and text
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;
const diskLabel = document.getElementById('disk-label');
const diskText = document.getElementById('disk-text');
const diskSubtext = document.getElementById('disk-subtext');
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);
}
if (diskLabel) {
const color = usedPct >= 95 ? 'red' : (usedPct >= 85 ? 'yellow' : 'green');
diskLabel.className = 'ui ' + color + ' large label';
}
if (diskText) {
diskText.textContent = Math.round(usedPct) + '% used';
}
if (diskSubtext) {
diskSubtext.textContent = formatBytes(data.disk.free_bytes) + ' free of ' + formatBytes(data.disk.total_bytes);
}
}
// Update bandwidth tile
// Update bandwidth tile - only change class and text
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;
const bwLabel = document.getElementById('bw-label');
const bwText = document.getElementById('bw-text');
const bwSubtext = document.getElementById('bw-subtext');
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;
if (bwLabel) {
const color = mbps >= 100 ? 'green' : (mbps >= 10 ? 'blue' : 'yellow');
bwLabel.className = 'ui ' + color + ' large label';
}
if (bwText) {
bwText.textContent = Math.round(mbps) + ' Mbps';
}
if (bwSubtext && 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();
}
bwSubtext.textContent = text;
}
}
})