import { useEffect, useRef } from 'preact/hooks';
import { signal } from '@preact/signals';
import { localFetch } from '../../lib/api.js';
const info = signal(null); const status = signal(null); const busy = signal(false);
function fmtCheckedAt(iso) {
if (!iso) return 'Not checked yet.';
try {
return 'Last checked ' + new Date(iso).toLocaleString();
} catch (_) {
return 'Last checked ' + iso;
}
}
export function UpdateCard() {
const fromRef = useRef('');
useEffect(() => {
load(false);
}, []);
const show = (msg, kind) => (status.value = { msg, kind });
async function load(force) {
const path = force ? '/api/update/check' : '/api/update/status';
try {
const res = await localFetch(path, force ? { method: 'POST' } : {});
if (!res.ok) throw new Error('HTTP ' + res.status);
info.value = await res.json();
if (force) show('Checked crates.io.', 'ok');
} catch (err) {
show('Update check failed: ' + err.message, 'error');
}
}
async function watchForNewVersion(fromVersion, logPath) {
const deadline = Date.now() + 600000; show('Updating… the service will restart. Watching for the new version.', 'ok');
busy.value = true;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 3000));
try {
const res = await localFetch('/api/identify', {});
if (res.ok) {
const id = await res.json();
if (id.version && id.version !== fromVersion) {
show(`Updated to ${id.version}. Reload to pick up the new UI.`, 'ok');
busy.value = false;
if (confirm(`mobux updated to ${id.version}. Reload now?`)) location.reload();
return;
}
}
} catch (_) {
}
}
show(
'Timed out after 10 minutes waiting for the new version. It may still be building, or it ' +
'rolled back — check the update log on the host: ' + (logPath || 'mobux-update.log'),
'error',
);
busy.value = false;
}
async function run() {
const fromVersion = info.value?.current || fromRef.current;
busy.value = true;
show('Starting update…', 'ok');
try {
const res = await localFetch('/api/update/run', { method: 'POST' });
if (res.status === 202) {
const body = await res.json().catch(() => ({}));
watchForNewVersion(fromVersion, body.log);
return;
}
let msg = 'HTTP ' + res.status;
try {
const body = await res.json();
if (body && body.error && body.error.message) msg = body.error.message;
} catch (_) {}
show('Update could not start: ' + msg, 'error');
busy.value = false;
} catch (err) {
show('Update could not start: ' + err.message, 'error');
busy.value = false;
}
}
const s = info.value || {};
const available = !!s.available;
return (
<section class="settings-card" id="update">
<h2>Software update</h2>
<p class="settings-lede">
mobux checks crates.io for newer published versions. Updating installs the new version with{' '}
<code>cargo install</code>, restarts the systemd service, health-checks it, and rolls back
automatically if the new version doesn't come up. This acts on <strong>this host only</strong>{' '}
— to update a peer, open its own settings page.
</p>
<div class="settings-row">
<span class="settings-label">
<strong>Current version</strong>
<small>This running binary on {typeof location !== 'undefined' ? location.hostname : ''}.</small>
</span>
<span class="settings-value">{s.current || '…'}</span>
</div>
<div class="settings-row">
<span class="settings-label">
<strong>Latest version</strong>
<small>{s.error ? 'Check failed: ' + s.error : fmtCheckedAt(s.checkedAt)}</small>
</span>
<span class={'settings-value' + (available ? ' settings-value--new' : '')}>
{s.latest || '…'}
</span>
</div>
<div class="shell-card-actions">
<button type="button" disabled={busy.value} onClick={() => load(true)}>
Check for updates
</button>
{available && (
<button type="button" disabled={busy.value} onClick={run}>
Update now → {s.latest}
</button>
)}
</div>
{status.value && (
<div class="settings-status" style={{ color: status.value.kind === 'error' ? '#f87171' : '' }}>
{status.value.msg}
</div>
)}
</section>
);
}