feat(daemon): add automatic update mechanism (Updater), switch to system journal logs, and expose update controls in the UI
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user