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
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user