import type { EnvironmentId, ServerSelfUpdateCapability } from "@t3tools/contracts"; import type { ServerUpdateState } from "@t3tools/client-runtime/state/server"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { CheckIcon } from "lucide-react"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { cn } from "~/lib/utils"; import { serverEnvironment } from "~/state/server"; import { useAtomCommand } from "~/state/use-atom-command"; import { manualServerUpdateCommand } from "~/versionSkew"; import { Button } from "./ui/button"; import { toastManager } from "./ui/toast"; const UPDATE_STEPS = [ { stage: "downloading", label: "Download", activeLabel: "Downloading" }, { stage: "installing", label: "Install", activeLabel: "Installing" }, { stage: "resuming", label: "Resume", activeLabel: "Resuming" }, ] as const; const pendingUpdateEnvironmentIds = new Set(); function updateFailureMessage(error: unknown): string { return error instanceof Error ? error.message : "Server update failed."; } /** * Monochrome step rail for an in-flight server update. The update is a * wait, not a warning: done steps recede to gray, the active step is the * only bright element and pulses. Failure keeps the rail but surfaces the * error below it. */ export function ServerUpdateProgress({ fromVersion, state, }: { readonly fromVersion: string; readonly state: Exclude; }) { const currentIndex = UPDATE_STEPS.findIndex(({ stage }) => stage === state.stage); return (

{fromVersion} {state.targetVersion}

    {UPDATE_STEPS.map((step, index) => { const complete = index < currentIndex; const current = index === currentIndex; const failed = current && state.status === "failed"; const running = current && state.status === "running"; return (
  1. {complete ? (
  2. ); })}
{state.status === "failed" ? (

{state.message}

) : null}
); } /** * Offers the update path advertised by a version-skewed server. Self-updates * delegate their full lifecycle to client-runtime so this component can * unmount during reconnect without losing operation state. */ export function ServerUpdateAction({ environmentId, serverLabel, selfUpdate, targetVersion, label = "Update server", }: { readonly environmentId: EnvironmentId; readonly serverLabel: string; readonly selfUpdate: ServerSelfUpdateCapability | null; readonly targetVersion: string; readonly label?: string; }) { const updateServer = useAtomCommand(serverEnvironment.updateServer, { reportFailure: false, }); const { copyToClipboard } = useCopyToClipboard<{ command: string }>({ target: "update command", onCopy: ({ command }) => { toastManager.add({ type: "success", title: "Update command copied", description: `Run \`${command}\` on ${serverLabel} to update it.`, }); }, onError: (error) => { toastManager.add({ type: "error", title: "Could not copy update command", description: error.message, }); }, }); const handleUpdate = async () => { if (pendingUpdateEnvironmentIds.has(environmentId)) { return; } pendingUpdateEnvironmentIds.add(environmentId); try { const result = await updateServer({ environmentId, input: { targetVersion }, }); if (result._tag === "Failure") { if (isAtomCommandInterrupted(result)) { return; } toastManager.add({ type: "error", title: "Server update failed", description: updateFailureMessage(squashAtomCommandFailure(result)), }); return; } toastManager.add({ type: "success", title: `${serverLabel} updated`, description: `Reconnected on t3@${result.value.targetVersion}.`, }); } finally { pendingUpdateEnvironmentIds.delete(environmentId); } }; if (selfUpdate === "desktop-managed") { return ( Update the desktop app on that machine to update this server. ); } if (selfUpdate === null) { const command = manualServerUpdateCommand(targetVersion); return ( ); } return ( ); }