evOo-local/apps/web/src/components/ComputerMaintenanceActions.tsx
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

159 lines
5.0 KiB
TypeScript

import { Trans, useLingui } from "@lingui/react/macro";
import type { ComputerStatus } from "@rakazo/contracts";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@rakazo/ui-web";
import { MoreHorizontal } from "lucide-react";
import { useState } from "react";
import { computerUpdates } from "../lib/computer-updates";
import { rpc } from "../lib/rpc";
type Action = "recover" | "reset" | "update";
export function ComputerMaintenanceActions({
botId,
computer,
onChanged,
}: {
botId: string;
computer: ComputerStatus | null;
onChanged: () => Promise<void>;
}) {
const { t } = useLingui();
const [menuOpen, setMenuOpen] = useState(false);
const [pending, setPending] = useState<Action | null>(null);
const [confirmReset, setConfirmReset] = useState(false);
const [error, setError] = useState<string | null>(null);
if (!computer) return null;
const busy = Boolean(computer.busyBotName) || computer.state === "booting";
const showRecover =
computer.state === "error" ||
computer.state === "running" ||
computer.state === "suspended" ||
computer.state === "stopped";
const showReset = showRecover;
const showUpdate = computer.canUpdate;
const hasActions = showRecover || showReset || showUpdate;
if (!hasActions) return null;
async function run(action: Action) {
setPending(action);
setError(null);
try {
if (action === "recover") await computerUpdates.start(botId, "recover");
else if (action === "reset") await rpc.computer.reset({ botId });
else await computerUpdates.start(botId);
setConfirmReset(false);
setMenuOpen(false);
await onChanged();
} catch (err) {
setError(err instanceof Error ? err.message : t`Could not update computer`);
} finally {
setPending(null);
}
}
function openResetConfirm() {
setError(null);
setMenuOpen(false);
setConfirmReset(true);
}
// Escape closes the dialog inside the popup, so Shell's Escape handler does not also
// close the computer overlay.
const resetDialog = (
<AlertDialog
open={confirmReset}
onOpenChange={(open) => {
if (!open) setConfirmReset(false);
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans>Reset computer?</Trans>
</AlertDialogTitle>
<AlertDialogDescription>
<Trans>Restore the last saved workspace. Unsaved work on the computer is lost.</Trans>
</AlertDialogDescription>
</AlertDialogHeader>
{error ? <p className="text-ui-body-sm text-destructive">{error}</p> : null}
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>Cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction disabled={pending !== null} onClick={() => void run("reset")}>
{pending === "reset" ? <Trans>Resetting…</Trans> : <Trans>Reset</Trans>}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
return (
<>
<DropdownMenu
open={menuOpen}
onOpenChange={(open) => {
if (open) setError(null);
setMenuOpen(open);
}}
>
<DropdownMenuTrigger
data-testid="computer-more-button"
aria-label={t`More computer actions`}
disabled={busy && pending === null}
render={<Button variant="ghost" size="icon-sm" className="text-muted-foreground" />}
>
<MoreHorizontal />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
data-testid="computer-more-menu"
className="w-auto min-w-44"
>
{showRecover ? (
<DropdownMenuItem
closeOnClick={false}
disabled={busy || pending !== null}
onClick={() => void run("recover")}
>
{pending === "recover" ? <Trans>Recovering…</Trans> : <Trans>Recover computer</Trans>}
</DropdownMenuItem>
) : null}
{showReset ? (
<DropdownMenuItem disabled={busy || pending !== null} onClick={openResetConfirm}>
{pending === "reset" ? <Trans>Resetting…</Trans> : <Trans>Reset computer</Trans>}
</DropdownMenuItem>
) : null}
{showUpdate ? (
<DropdownMenuItem
closeOnClick={false}
disabled={busy || pending !== null}
onClick={() => void run("update")}
>
{pending === "update" ? <Trans>Updating…</Trans> : <Trans>Update computer</Trans>}
</DropdownMenuItem>
) : null}
{error ? <p className="px-1.5 py-1 text-ui-body-sm text-destructive">{error}</p> : null}
</DropdownMenuContent>
</DropdownMenu>
{resetDialog}
</>
);
}