feat(daemon): add automatic update mechanism (Updater), switch to system journal logs, and expose update controls in the UI

This commit is contained in:
2026-01-09 16:55:43 +00:00
parent 1e86acff55
commit 6a3be55cee
8 changed files with 449 additions and 33 deletions

View File

@@ -104,6 +104,31 @@ export class UIServer {
return new Response(JSON.stringify(result), { headers });
}
if (path === '/api/updates') {
const updates = await this.daemon.getUpdateInfo();
return new Response(JSON.stringify(updates), { headers });
}
if (path === '/api/updates/check' && req.method === 'POST') {
await this.daemon.checkForUpdates();
const updates = await this.daemon.getUpdateInfo();
return new Response(JSON.stringify(updates), { headers });
}
if (path === '/api/upgrade' && req.method === 'POST') {
try {
const body = await req.json();
const version = body.version;
if (!version) {
return new Response(JSON.stringify({ success: false, message: 'Version required' }), { headers });
}
const result = await this.daemon.upgradeToVersion(version);
return new Response(JSON.stringify(result), { headers });
} catch (error) {
return new Response(JSON.stringify({ success: false, message: String(error) }), { headers });
}
}
return new Response(JSON.stringify({ error: 'Not Found' }), {
status: 404,
headers,
@@ -347,6 +372,18 @@ export class UIServer {
</button>
<div id="control-status" style="margin-top: 8px; font-size: 12px; color: var(--text-dim);"></div>
</div>
<div class="card">
<h2>Updates</h2>
<div class="stat">
<div class="stat-label">Current Version</div>
<div class="stat-value" id="current-version">-</div>
</div>
<div id="updates-list" style="margin: 12px 0;"></div>
<div id="auto-upgrade-status" style="font-size: 12px; color: var(--text-dim);"></div>
<button class="btn btn-primary" onclick="checkForUpdates()" style="margin-top: 8px;">
Check for Updates
</button>
</div>
<div class="card">
<h2>Input Devices</h2>
<div id="input-devices-list"></div>
@@ -362,7 +399,7 @@ export class UIServer {
<div class="card" style="grid-column: 1 / -1;">
<div class="tabs">
<div class="tab active" onclick="switchTab('daemon')">Daemon Logs</div>
<div class="tab" onclick="switchTab('serial')">Serial Console</div>
<div class="tab" onclick="switchTab('serial')">System Logs</div>
</div>
<div id="daemon-tab" class="tab-content active">
<div class="logs" id="logs"></div>
@@ -522,13 +559,13 @@ export class UIServer {
logsEl.scrollTop = logsEl.scrollHeight;
}
// Serial Logs
if (data.serialLogs) {
// System Logs
if (data.systemLogs) {
const serialEl = document.getElementById('serial-logs');
if (data.serialLogs.length === 0) {
serialEl.innerHTML = '<div style="color: var(--text-dim);">No serial data available</div>';
if (data.systemLogs.length === 0) {
serialEl.innerHTML = '<div style="color: var(--text-dim);">No system logs available</div>';
} else {
serialEl.innerHTML = data.serialLogs.map(l =>
serialEl.innerHTML = data.systemLogs.map(l =>
'<div class="log-entry">' + l + '</div>'
).join('');
serialEl.scrollTop = serialEl.scrollHeight;
@@ -590,6 +627,77 @@ export class UIServer {
});
}
function checkForUpdates() {
fetch('/api/updates/check', { method: 'POST' })
.then(r => r.json())
.then(updateUpdatesUI)
.catch(err => console.error('Failed to check updates:', err));
}
function upgradeToVersion(version) {
if (!confirm('Upgrade to version ' + version + '? The daemon will restart.')) return;
fetch('/api/upgrade', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: version })
})
.then(r => r.json())
.then(result => {
if (result.success) {
document.getElementById('auto-upgrade-status').textContent = result.message;
} else {
alert('Upgrade failed: ' + result.message);
}
})
.catch(err => alert('Upgrade error: ' + err));
}
function updateUpdatesUI(data) {
document.getElementById('current-version').textContent = 'v' + data.currentVersion;
const list = document.getElementById('updates-list');
const newerReleases = data.releases.filter(r => r.isNewer);
if (newerReleases.length === 0) {
list.innerHTML = '<div style="color: var(--text-dim);">No updates available</div>';
} else {
list.innerHTML = newerReleases.map(r =>
'<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border);">' +
'<span>v' + r.version + ' <span style="color: var(--text-dim);">(' + formatAge(r.ageHours) + ')</span></span>' +
'<button class="btn btn-primary" style="padding: 4px 12px; margin: 0;" onclick="upgradeToVersion(\\'' + r.version + '\\')">Upgrade</button>' +
'</div>'
).join('');
}
const autoStatus = document.getElementById('auto-upgrade-status');
if (data.autoUpgrade.targetVersion) {
if (data.autoUpgrade.waitingForStability) {
autoStatus.textContent = 'Auto-upgrade to v' + data.autoUpgrade.targetVersion + ' in ' + data.autoUpgrade.scheduledIn + ' (stability period)';
} else {
autoStatus.textContent = 'Auto-upgrade to v' + data.autoUpgrade.targetVersion + ' pending...';
}
} else {
autoStatus.textContent = data.lastCheck ? 'Last checked: ' + new Date(data.lastCheck).toLocaleTimeString() : '';
}
}
function formatAge(hours) {
if (hours < 1) return Math.round(hours * 60) + 'm ago';
if (hours < 24) return Math.round(hours) + 'h ago';
return Math.round(hours / 24) + 'd ago';
}
// Fetch updates info periodically
function fetchUpdates() {
fetch('/api/updates')
.then(r => r.json())
.then(updateUpdatesUI)
.catch(err => console.error('Failed to fetch updates:', err));
}
fetchUpdates();
setInterval(fetchUpdates, 60000); // Check every minute
// Initial fetch
fetch('/api/status')
.then(r => r.json())