/** * @file NineRouterUpdate.tsx * @description **Update 9router** on the AI Proxy page (every user's; beside * Config 9router): a button — dotted when npm has a newer 9router — and a * dialog that runs the server's three-step job (`server/lib/nine-router-update.js`): * disconnect and shut 9router down, `npm install -g 9router@latest`, start it * again hidden (tray, no browser) — the last only when it was running before. * The dialog shows why an update cannot run right now (the server's `blocker`, * with the command to run by hand when sudo needs a password) and follows the * job by polling until it ends. Closing the dialog does not stop the job. * @author Nguyễn Ngọc Trí Vĩ */ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ArrowUpCircle, Check, Circle, Copy, Loader2, MinusCircle, X } from "lucide-react"; import { api, type NineRouterUpdateStatus, type NineRouterUpdateStep } from "../../lib/api"; import { copyText } from "../../lib/clipboard"; import { useDialog } from "../../hooks/useDialog"; const POLL_MS = 1000; function StepIcon({ status }: { status: NineRouterUpdateStep["status"] }) { if (status === "running") return ; if (status === "done") return ; if (status === "failed") return ; if (status === "skipped") return ; return ; } export function NineRouterUpdate() { const { t } = useTranslation("settings"); const [status, setStatus] = useState(null); const [open, setOpen] = useState(false); const [starting, setStarting] = useState(false); const [error, setError] = useState(null); const dialogRef = useRef(null); useDialog(open, dialogRef, () => setOpen(false)); const load = useCallback(async (fresh = false) => { try { setStatus(await api.aiProxies.nineRouterUpdateStatus(fresh)); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }, []); useEffect(() => { void load(); }, [load]); const running = status?.job?.status === "running"; // Follow a running job — also one started from another tab. useEffect(() => { if (!running) return; const timer = window.setInterval(() => void load(), POLL_MS); return () => window.clearInterval(timer); }, [running, load]); const openDialog = () => { setError(null); setOpen(true); void load(true); }; const start = async () => { setStarting(true); setError(null); try { const job = await api.aiProxies.nineRouterUpdate(); setStatus((s) => (s ? { ...s, job } : s)); } catch (e) { setError(e instanceof Error ? e.message : String(e)); void load(true); } finally { setStarting(false); } }; if (!status?.installed) return null; const job = status.job; const blocker = status.blocker; return ( <> {open && (

{t("aiProxy.update9router")}

{t("aiProxy.update9routerCurrent")}
{status.version ?? "—"}
{t("aiProxy.update9routerLatest")}
{status.latest ?? "—"}
{t("aiProxy.update9routerState")}
{status.running ? status.managedBy === "systemd" ? t("aiProxy.update9routerRunningUnit") : t("aiProxy.update9routerRunning") : t("aiProxy.update9routerStopped")}

{status.running ? t("aiProxy.update9routerExplain") : t("aiProxy.update9routerExplainStopped")}

{job && (
    {job.steps.map((s) => (
  1. {t(`aiProxy.update9routerStep.${s.id}`)} {s.message && ( {s.message} )}
  2. ))}
)} {job && job.status !== "running" && (

{job.status !== "done" ? t("aiProxy.update9routerFailed") : job.steps.find((s) => s.id === "relaunch")?.status === "skipped" ? t("aiProxy.update9routerDoneStopped", { version: job.to ?? "—" }) : t("aiProxy.update9routerDone", { version: job.to ?? "—" })}

)} {job?.log && (
{t("aiProxy.update9routerLog")}
                  {job.log}
                
)} {blocker && !running && blocker.code !== "EUPTODATE" && (

{blocker.message}

{blocker.command && (
{blocker.command}
)}
)} {blocker?.code === "EUPTODATE" && !running && (

{t("aiProxy.update9routerUpToDate")}

)} {error && (

{error}

)}
)} ); }