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 id="updateCurrent" 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
id="updateLatest"
class={"settings-value" + (available ? " settings-value--new" : "")}
>
{s.latest || "…"}
</span>
</div>
<div class="shell-card-actions">
<button
id="updateCheckBtn"
type="button"
disabled={busy.value}
onClick={() => load(true)}
>
Check for updates
</button>
{available && (
<button
id="updateRunBtn"
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>
);
}