Implement robust auto-update system for GuruConnect agent

Features:
- Agent checks for updates periodically (hourly) during idle
- Admin can trigger immediate updates via dashboard "Update Agent" button
- Silent updates with in-place binary replacement (no reboot required)
- SHA-256 checksum verification before installation
- Semantic version comparison

Server changes:
- New releases table for tracking available versions
- GET /api/version endpoint for agent polling (unauthenticated)
- POST /api/machines/:id/update endpoint for admin push updates
- Release management API (/api/releases CRUD)
- Track agent_version in machine status

Agent changes:
- New update.rs module with download/verify/install/restart logic
- Handle ADMIN_UPDATE WebSocket command for push updates
- --post-update flag for cleanup after successful update
- Periodic update check in idle loop (persistent agents only)
- agent_version included in AgentStatus messages

Dashboard changes:
- Version display in machine detail panel
- "Update Agent" button for each connected machine

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 09:31:23 -07:00
parent 7df824c2ca
commit 4e5328fe4a
15 changed files with 1399 additions and 18 deletions

View File

@@ -908,6 +908,7 @@
const statusText = m.is_online ? 'Online' : 'Offline';
const connectDisabled = m.is_online ? '' : 'disabled';
const connectTitle = m.is_online ? '' : 'title="Agent is offline"';
const versionText = m.agent_version || 'Unknown';
container.innerHTML =
'<div class="detail-section">' +
@@ -915,6 +916,7 @@
'<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value" style="color: ' + statusColor + ';">' + statusText + '</span></div>' +
'<div class="detail-row"><span class="detail-label">Agent ID</span><span class="detail-value">' + m.agent_id.slice(0,8) + '...</span></div>' +
'<div class="detail-row"><span class="detail-label">Session ID</span><span class="detail-value">' + m.id.slice(0,8) + '...</span></div>' +
'<div class="detail-row"><span class="detail-label">Version</span><span class="detail-value">' + escapeHtml(versionText) + '</span></div>' +
'<div class="detail-row"><span class="detail-label">Connected</span><span class="detail-value">' + started + '</span></div>' +
'<div class="detail-row"><span class="detail-label">Viewers</span><span class="detail-value">' + m.viewer_count + '</span></div>' +
'</div>' +
@@ -923,6 +925,7 @@
'<button class="btn btn-primary" style="width: 100%; margin-bottom: 8px;" onclick="connectToMachine(\'' + m.id + '\')" ' + connectDisabled + ' ' + connectTitle + '>Connect</button>' +
'<button class="btn btn-outline" style="width: 100%; margin-bottom: 8px;" onclick="openChat(\'' + m.id + '\', \'' + (m.agent_name || 'Client').replace(/'/g, "\\'") + '\')" ' + connectDisabled + '>Chat</button>' +
'<button class="btn btn-outline" style="width: 100%; margin-bottom: 8px;" disabled>Transfer Files</button>' +
'<button class="btn btn-outline" style="width: 100%; margin-bottom: 8px;" onclick="triggerUpdate(\'' + m.agent_id + '\', \'' + (m.agent_name || m.agent_id).replace(/'/g, "\\'") + '\')" ' + connectDisabled + '>Update Agent</button>' +
'<button class="btn btn-outline" style="width: 100%; color: hsl(0, 62.8%, 50%);" onclick="disconnectMachine(\'' + m.id + '\', \'' + (m.agent_name || m.agent_id).replace(/'/g, "\\'") + '\')">Disconnect</button>' +
'</div>';
}
@@ -1043,6 +1046,25 @@
}
}
async function triggerUpdate(agentId, machineName) {
if (!confirm("Send update command to " + machineName + "?\n\nThe agent will download and install the latest version, then restart.")) return;
try {
const response = await fetch("/api/machines/" + agentId + "/update", {
method: "POST",
headers: { "Content-Type": "application/json" }
});
if (response.ok) {
const result = await response.json();
alert("Update command sent to " + machineName + ".\n\n" + (result.message || "Agent will update shortly."));
} else {
const errorText = await response.text();
alert("Failed to trigger update: " + errorText);
}
} catch (err) {
alert("Error triggering update: " + err.message);
}
}
// Refresh machines every 5 seconds
loadMachines();
setInterval(loadMachines, 5000);