Ghost Shell

JavaScript NOASSERTION

Stars
0
Forks
0
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
src/pages/hosts.jsx
import Icons from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
  InputGroup,
  InputGroupButton,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group";
import { Label } from "@/components/ui/label";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetFooter,
} from "@/components/ui/sheet";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Switch } from "@/components/ui/switch";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import DashboardLayout from "@/layouts/dashboard";
import {
  ClipboardIcon,
  EditIcon,
  EyeIcon,
  EyeOffIcon,
  LockIcon,
  MoreHorizontalIcon,
  PlusIcon,
  SearchIcon,
  Server,
  ServerCog,
  ServerCogIcon,
  TerminalIcon,
  TrashIcon,
  UserIcon,
  Save,
  X,
  KeyIcon,
  Loader2,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import React from "react";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { invoke } from "@tauri-apps/api/core";
import { useTerminals } from "@/hooks/use-terminals";

export default function Hosts() {
  const { openSession } = useTerminals();
  const [hosts, setHosts] = React.useState([]);
  const [keys, setKeys] = React.useState([]);
  const [searchQuery, setSearchQuery] = React.useState("");
  
  const [showPassword, setShowPassword] = React.useState(false);
  const [openAddHosts, setOpenAddHosts] = React.useState(false);
  const [openGenerateKey, setOpenGenerateKey] = React.useState(false);

  // Form & Sheet Mode States
  const [sheetMode, setSheetMode] = React.useState("add"); // "add" | "edit"
  const [selectedHost, setSelectedHost] = React.useState(null);
  const [hostToDelete, setHostToDelete] = React.useState(null);
  const [deleting, setDeleting] = React.useState(false);
  
  const [hostName, setHostName] = React.useState("");
  const [hostAddress, setHostAddress] = React.useState("");
  const [hostPort, setHostPort] = React.useState("22");
  const [hostUsername, setHostUsername] = React.useState("");
  const [hostPassword, setHostPassword] = React.useState("");
  const [hostKeyId, setHostKeyId] = React.useState(null);
  
  const [keyDropdownOpen, setKeyDropdownOpen] = React.useState(false);
  const [hostStatuses, setHostStatuses] = React.useState({});

  // Generate Key Form States
  const [genName, setGenName] = React.useState("");
  const [genPassphrase, setGenPassphrase] = React.useState("");
  const [genSavePassphrase, setGenSavePassphrase] = React.useState(false);
  const [showGenPassphrase, setShowGenPassphrase] = React.useState(false);
  const [keyType, setKeyType] = React.useState("rsa");
  const [rsaKeySize, setRsaKeySize] = React.useState("4096");
  const [ecdsaKeySize, setEcdsaKeySize] = React.useState("521");

  const [loading, setLoading] = React.useState(false);
  const [attemptedSubmit, setAttemptedSubmit] = React.useState(false);

  const handleShowPassword = () => {
    setShowPassword(!showPassword);
  };

  const checkReachability = async (host) => {
    try {
      const online = await invoke("check_host_reachability", {
        address: host.address,
        port: host.port,
      });
      setHostStatuses((prev) => ({
        ...prev,
        [host.id]: online ? "online" : "offline",
      }));
    } catch {
      setHostStatuses((prev) => ({
        ...prev,
        [host.id]: "offline",
      }));
    }
  };

  const loadHosts = async () => {
    try {
      const list = await invoke("get_hosts");
      setHosts(list || []);
      list.forEach((h) => {
        setHostStatuses((prev) => ({ ...prev, [h.id]: "checking" }));
        checkReachability(h);
      });
    } catch (err) {
      console.error("Failed to load hosts:", err);
    }
  };

  const loadKeys = async () => {
    try {
      const list = await invoke("get_keys");
      setKeys(list || []);
    } catch (err) {
      console.error("Failed to load keys:", err);
    }
  };

  React.useEffect(() => {
    loadHosts();
    loadKeys();
  }, []);

  React.useEffect(() => {
    if (hosts.length === 0) return;
    const interval = setInterval(() => {
      hosts.forEach((h) => {
        checkReachability(h);
      });
    }, 30000);
    return () => clearInterval(interval);
  }, [hosts]);

  const handleAddHost = async () => {
    setAttemptedSubmit(true);
    if (!hostAddress || !hostUsername) {
      alert("Address and Username are required.");
      return;
    }

    setLoading(true);

    const entry = {
      id: sheetMode === "edit" ? selectedHost.id : 0,
      name: hostName || hostAddress,
      address: hostAddress,
      port: parseInt(hostPort) || 22,
      username: hostUsername,
      password: hostPassword || null,
      key_id: hostKeyId,
      created_at: sheetMode === "edit" ? selectedHost.created_at : new Date().toISOString().split("T")[0],
      updated_at: new Date().toISOString().split("T")[0],
      os: "Linux",
    };

    try {
      await invoke("add_host", { entry });
      setOpenAddHosts(false);
      clearForm();
      loadHosts();
    } catch (err) {
      alert("Failed to save host: " + err);
    } finally {
      setLoading(false);
    }
  };

  const handleGenerateKey = async () => {
    if (!genName) {
      alert("Key name is required.");
      return;
    }
    
    const size = keyType === "rsa" ? rsaKeySize : keyType === "ecdsa" ? ecdsaKeySize : "";
    setLoading(true);
    try {
      const newKey = await invoke("generate_key", {
        name: genName,
        keyType: keyType,
        size: size,
        passphrase: genPassphrase || null,
        savePassphrase: genSavePassphrase,
      });
      setOpenGenerateKey(false);
      clearGenStates();
      await loadKeys();
      // Auto-select the newly generated key
      setHostKeyId(newKey.id);
    } catch (err) {
      alert("Failed to generate key: " + err);
    } finally {
      setLoading(false);
    }
  };

  const handleDeleteHost = async () => {
    if (!hostToDelete) return;
    setDeleting(true);
    try {
      await invoke("delete_host", { id: hostToDelete.id });
      setHostToDelete(null);
      loadHosts();
    } catch (err) {
      alert("Failed to delete host: " + err);
    } finally {
      setDeleting(false);
    }
  };

  const clearForm = () => {
    setHostName("");
    setHostAddress("");
    setHostPort("22");
    setHostUsername("");
    setHostPassword("");
    setHostKeyId(null);
    setSelectedHost(null);
    setAttemptedSubmit(false);
  };

  const clearGenStates = () => {
    setGenName("");
    setGenPassphrase("");
    setGenSavePassphrase(false);
  };

  const filteredHosts = hosts.filter((h) =>
    h.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
    h.address.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <DashboardLayout>
      <div className="flex flex-col gap-2">
        <div className="flex flex-row gap-2 items-center justify-between">
          <InputGroup className="max-w-sm">
            <InputGroupAddon>
              <SearchIcon />
            </InputGroupAddon>
            <InputGroupInput
              placeholder="Search hosts"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
            />
          </InputGroup>
          <div className="flex flex-col gap-2">
            <Tooltip>
              <TooltipTrigger asChild>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => {
                    setSheetMode("add");
                    clearForm();
                    setOpenAddHosts(true);
                  }}
                >
                  <PlusIcon />
                  <span>Add Hosts</span>
                </Button>
              </TooltipTrigger>
              <TooltipContent side="left">
                <p>Add New Hosts</p>
              </TooltipContent>
            </Tooltip>
          </div>
        </div>

        {filteredHosts.length === 0 ? (
          <div className="flex flex-col items-center justify-center p-8 bg-sidebar border rounded-lg">
            <ServerCogIcon className="size-8 text-muted-foreground mb-2 animate-bounce" />
            <p className="text-sm font-medium text-foreground">
              No hosts found
            </p>
            <p className="text-xs text-muted-foreground">
              Add a secure SSH host connection to get started.
            </p>
          </div>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
            {filteredHosts.map((host) => (
              <div
                className="border bg-sidebar rounded-lg px-3 py-2 flex flex-row gap-2 cursor-pointer"
                key={host.id}
                onClick={() => openSession(host)}
              >
                <div className="size-10 bg-primary rounded-md flex items-center justify-center shrink-0 border border-primary/50">
                  <ServerCogIcon className="size-5 text-primary-foreground" />
                </div>
                <div className="flex flex-col space-y-1 items-start justify-center flex-1 min-w-0">
                  <div className="flex items-center gap-1.5 w-full min-w-0">
                    <p className="text-sm font-medium leading-none text-foreground truncate flex-1">
                      {host.name}
                    </p>
                    <div
                      className={`size-2 rounded-full shrink-0 ${
                        hostStatuses[host.id] === "online"
                          ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.7)]"
                          : hostStatuses[host.id] === "offline"
                            ? "bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.4)]"
                            : "bg-muted-foreground/40 animate-pulse"
                      }`}
                      title={
                        hostStatuses[host.id] === "online"
                          ? "Reachable"
                          : hostStatuses[host.id] === "offline"
                            ? "Unreachable"
                            : "Checking reachability..."
                      }
                    />
                  </div>
                  <p className="text-xs leading-none text-foreground/80 truncate w-full">
                    {host.username}@{host.address}:{host.port}
                  </p>
                </div>
                <div className="flex flex-col space-y-1 items-end justify-center">
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button
                        size="icon-xs"
                        variant="outline"
                        className="rounded-xs cursor-pointer"
                      >
                        <MoreHorizontalIcon className="size-3.5" />
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent
                      side="bottom"
                      align="end"
                      className="rounded-xs w-fit px-2 space-y-1"
                    >
                      <DropdownMenuItem
                        className="cursor-pointer"
                        onSelect={() => openSession(host)}
                      >
                        <TerminalIcon className="size-3.5" />
                        <span className="text-sm text-muted-foreground shrink-0">
                          Connect to Host
                        </span>
                      </DropdownMenuItem>
                      <DropdownMenuItem
                        className="cursor-pointer"
                        onSelect={() => {
                          setSelectedHost(host);
                          setHostName(host.name);
                          setHostAddress(host.address);
                          setHostPort(host.port.toString());
                          setHostUsername(host.username);
                          setHostPassword(host.password || "");
                          setHostKeyId(host.key_id || null);
                          setSheetMode("edit");
                          setOpenAddHosts(true);
                        }}
                      >
                        <EditIcon className="size-3.5" />
                        <span className="text-sm text-muted-foreground shrink-0">
                          Edit Host
                        </span>
                      </DropdownMenuItem>
                      <DropdownMenuSeparator />
                      <DropdownMenuItem
                        className="cursor-pointer text-destructive focus:text-destructive"
                        onSelect={() => setHostToDelete(host)}
                      >
                        <TrashIcon className="size-3.5 text-destructive" />
                        <span className="text-sm text-destructive shrink-0">
                          Delete Host
                        </span>
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>

      {/* -------------------- ADD / EDIT HOST SHEET -------------------- */}
      <Sheet
        open={openAddHosts}
        onOpenChange={(open) =>
          !loading && ((!open && clearForm()) || setOpenAddHosts(open))
        }
      >
        <SheetContent
          className="rounded-l-xl overflow-hidden flex flex-col h-full"
          onPointerDownOutside={(e) => loading && e.preventDefault()}
          onEscapeKeyDown={(e) => loading && e.preventDefault()}
        >
          <SheetHeader className="bg-muted">
            <SheetTitle>
              {sheetMode === "add" ? "Add Hosts" : "Edit Host"}
            </SheetTitle>
            <SheetDescription>
              {sheetMode === "add"
                ? "Manage Hosts"
                : "Modify host connection details."}
            </SheetDescription>
          </SheetHeader>
          <div className="flex-1 overflow-y-auto flex flex-col gap-3 py-2 px-3">
            <div className="rounded-lg bg-muted p-4 space-y-3">
              <Label>Host Name</Label>
              <Input
                placeholder="My Staging Server"
                className="bg-background"
                value={hostName}
                onChange={(e) => setHostName(e.target.value)}
                disabled={loading}
              />
            </div>

            <div className="rounded-lg bg-muted p-4 space-y-3">
              <Label>Address</Label>
              <div className="flex items-center gap-2">
                <div className="size-10 bg-primary rounded-full flex items-center justify-center shrink-0">
                  <Server className="size-5.5 text-primary-foreground" />
                </div>
                <Input
                  placeholder="Server Address (e.g. 192.168.1.50)"
                  className="bg-background"
                  value={hostAddress}
                  onChange={(e) => setHostAddress(e.target.value)}
                  disabled={loading}
                  aria-invalid={attemptedSubmit && !hostAddress ? "true" : "false"}
                />
              </div>
            </div>

            <div className="rounded-lg bg-muted p-4 space-y-3">
              <div className="flex items-center gap-2 border-b pb-3 justify-between">
                <div className="size-10 bg-primary rounded-full flex items-center justify-center shrink-0">
                  <ServerCog className="size-5.5 text-primary-foreground" />
                </div>
                <div className="flex items-center gap-2">
                  <Label className="shrink-0">SSH Port</Label>
                  <Input
                    placeholder="22"
                    className="w-16 placeholder:text-center text-center no-spinner bg-background"
                    type="number"
                    value={hostPort}
                    onChange={(e) => setHostPort(e.target.value)}
                    disabled={loading}
                  />
                </div>
              </div>

              <div className="flex flex-col gap-3">
                <Label>Credentials</Label>
                <div className="flex flex-col space-y-3">
                  <InputGroup className="bg-background">
                    <InputGroupAddon>
                      <UserIcon />
                    </InputGroupAddon>
                    <InputGroupInput
                      placeholder="Username"
                      value={hostUsername}
                      onChange={(e) => setHostUsername(e.target.value)}
                      disabled={loading}
                      aria-invalid={attemptedSubmit && !hostUsername ? "true" : "false"}
                    />
                  </InputGroup>
                  <InputGroup className="bg-background">
                    <InputGroupAddon>
                      <LockIcon />
                    </InputGroupAddon>
                    <InputGroupInput
                      placeholder="Password (optional)"
                      type={showPassword ? "text" : "password"}
                      value={hostPassword}
                      onChange={(e) => setHostPassword(e.target.value)}
                      disabled={loading}
                    />
                    <InputGroupButton
                      type="button"
                      onClick={handleShowPassword}
                      disabled={loading}
                    >
                      {showPassword ? (
                        <EyeIcon className="size-4" />
                      ) : (
                        <EyeOffIcon className="size-4" />
                      )}
                    </InputGroupButton>
                  </InputGroup>

                  {/* Focus-triggered SSH Key Selector */}
                  <div className="flex flex-col gap-1.5 relative">
                    <Label className="text-xs text-muted-foreground">
                      SSH Key Pair (Optional)
                    </Label>
                    <Input
                      placeholder={
                        keys.length === 0
                          ? "No keys available (Click Create a Key)"
                          : "Select key pair..."
                      }
                      value={keys.find((k) => k.id === hostKeyId)?.name || ""}
                      onFocus={() => setKeyDropdownOpen(true)}
                      onBlur={() => {
                        // Small delay to allow click events to fire before blurring
                        setTimeout(() => setKeyDropdownOpen(false), 200);
                      }}
                      readOnly
                      disabled={loading}
                      className="bg-background cursor-pointer text-xs"
                    />
                    {keyDropdownOpen && (
                      <div className="absolute top-[56px] left-0 right-0 z-50 border bg-popover text-popover-foreground rounded-md shadow-md max-h-48 overflow-y-auto p-1 space-y-1">
                        {keys.length > 0 && (
                          <div
                            className="px-2 py-1 text-[10px] font-semibold text-muted-foreground border-b mb-1"
                            onMouseDown={(e) => e.preventDefault()}
                          >
                            Available Keychains
                          </div>
                        )}
                        {keys.map((k) => (
                          <div
                            key={k.id}
                            onMouseDown={(e) => e.preventDefault()}
                            onClick={() => {
                              setHostKeyId(k.id);
                              setKeyDropdownOpen(false);
                            }}
                            className={`px-2 py-1 text-xs rounded-sm cursor-pointer flex items-center justify-between ${
                              hostKeyId === k.id
                                ? "bg-primary text-primary-foreground font-semibold"
                                : "hover:bg-accent hover:text-accent-foreground text-foreground"
                            }`}
                          >
                            <span className="truncate flex-1 pr-2">{k.name}</span>
                            <span className="opacity-70 uppercase text-[9px] shrink-0">
                              ({k.type})
                            </span>
                          </div>
                        ))}
                        {hostKeyId && (
                          <div
                            onMouseDown={(e) => e.preventDefault()}
                            onClick={() => {
                              setHostKeyId(null);
                              setKeyDropdownOpen(false);
                            }}
                            className="px-2 py-1 text-xs rounded-sm text-destructive hover:bg-destructive/10 cursor-pointer font-medium"
                          >
                            None (Clear key)
                          </div>
                        )}
                        <div
                          onMouseDown={(e) => e.preventDefault()}
                          onClick={() => {
                            setKeyDropdownOpen(false);
                            setOpenGenerateKey(true);
                          }}
                          className="px-2 py-1 text-xs rounded-sm text-primary hover:bg-primary/10 cursor-pointer font-bold border-t flex items-center gap-1 mt-1"
                        >
                          <PlusIcon className="size-3.5" />
                          Create a Key
                        </div>
                      </div>
                    )}
                  </div>
                </div>
              </div>
            </div>
          </div>
          <SheetFooter className="bg-muted flex-row gap-2 py-2.5 px-3">
            <Button
              className="flex-1"
              onClick={handleAddHost}
              disabled={loading}
            >
              {loading ? (
                <Loader2 className="animate-spin size-4" />
              ) : (
                <Save className="size-4" />
              )}
              {loading ? "Saving Host..." : "Save Host"}
            </Button>
            <Button
              variant="destructive"
              className="flex-1"
              onClick={() => {
                setOpenAddHosts(false);
                clearForm();
              }}
              disabled={loading}
            >
              <X />
              Cancel
            </Button>
          </SheetFooter>
        </SheetContent>
      </Sheet>

      {/* -------------------- DYNAMIC GENERATE KEY SHEET -------------------- */}
      <Sheet
        open={openGenerateKey}
        onOpenChange={(open) =>
          !loading && ((!open && clearGenStates()) || setOpenGenerateKey(open))
        }
      >
        <SheetContent
          className="rounded-l-xl overflow-hidden flex flex-col h-full z-[100]"
          onPointerDownOutside={(e) => loading && e.preventDefault()}
          onEscapeKeyDown={(e) => loading && e.preventDefault()}
        >
          <SheetHeader className="bg-muted">
            <SheetTitle>Generate SSH Key</SheetTitle>
            <SheetDescription>
              Generate a new SSH public/private key pair to link to this host.
            </SheetDescription>
          </SheetHeader>
          <div className="p-4 overflow-y-auto space-y-4 flex-1">
            <div className="space-y-6 rounded-lg bg-muted p-4">
              <div className="space-y-2">
                <Label>Key Name</Label>
                <Input
                  placeholder="My Production Server"
                  className="bg-background"
                  value={genName}
                  onChange={(e) => setGenName(e.target.value)}
                  disabled={loading}
                />
              </div>
              <Tabs value={keyType} onValueChange={setKeyType}>
                <div className="space-y-2">
                  <Label>Key Type</Label>
                  <TabsList className="grid w-full grid-cols-3 bg-background">
                    <TabsTrigger value="rsa" disabled={loading}>
                      RSA
                    </TabsTrigger>
                    <TabsTrigger value="ed25519" disabled={loading}>
                      Ed25519
                    </TabsTrigger>
                    <TabsTrigger value="ecdsa" disabled={loading}>
                      ECDSA
                    </TabsTrigger>
                  </TabsList>
                </div>
                <TabsContent value="rsa" className="space-y-4">
                  <p className="text-xs text-muted-foreground">
                    Compatible with almost every SSH server. (Sizes less than
                    2048 are blocked for security).
                  </p>
                  <div className="space-y-2">
                    <Label>Key Size</Label>
                    <div className="grid grid-cols-2 gap-2">
                      {["2048", "4096"].map((size) => (
                        <Button
                          key={size}
                          type="button"
                          variant={rsaKeySize === size ? "default" : "outline"}
                          onClick={() => setRsaKeySize(size)}
                          disabled={loading}
                        >
                          {size}
                        </Button>
                      ))}
                    </div>
                  </div>
                </TabsContent>
                <TabsContent value="ecdsa" className="space-y-4">
                  <p className="text-xs text-muted-foreground">
                    Faster than RSA with strong security.
                  </p>
                  <div className="space-y-2">
                    <Label>Curve</Label>
                    <div className="grid grid-cols-3 gap-2">
                      {["256", "384", "521"].map((size) => (
                        <Button
                          key={size}
                          type="button"
                          variant={
                            ecdsaKeySize === size ? "default" : "outline"
                          }
                          onClick={() => setEcdsaKeySize(size)}
                          disabled={loading}
                        >
                          {size}
                        </Button>
                      ))}
                    </div>
                  </div>
                </TabsContent>
                <TabsContent value="ed25519" className="space-y-4">
                  <p className="text-xs text-muted-foreground">
                    Recommended for modern systems. Fast, compact, and highly
                    secure.
                  </p>
                </TabsContent>
              </Tabs>
            </div>
            <div className="flex flex-col gap-2 bg-muted p-4 rounded-lg">
              <Label>Passphrase</Label>
              <InputGroup>
                <InputGroupAddon>
                  <LockIcon className="size-4" />
                </InputGroupAddon>
                <InputGroupInput
                  placeholder="Enter a passphrase (optional)"
                  className="bg-background"
                  type={showGenPassphrase ? "text" : "password"}
                  value={genPassphrase}
                  onChange={(e) => setGenPassphrase(e.target.value)}
                  disabled={loading}
                />
                <InputGroupButton
                  onClick={() => setShowGenPassphrase(!showGenPassphrase)}
                  disabled={loading}
                >
                  {showGenPassphrase ? (
                    <EyeOffIcon className="size-4" />
                  ) : (
                    <EyeIcon className="size-4" />
                  )}
                </InputGroupButton>
              </InputGroup>
              <p className="text-xs text-muted-foreground">
                Enter a passphrase to protect your private key.
              </p>
              <div className="flex flex-row justify-between gap-2 mt-2">
                <Label
                  className="text-sm text-muted-foreground"
                  htmlFor="save-passphrase-switch"
                >
                  Save passphrase
                </Label>
                <Switch
                  id="save-passphrase-switch"
                  checked={genSavePassphrase}
                  onCheckedChange={setGenSavePassphrase}
                  disabled={loading}
                />
              </div>
            </div>
          </div>
          <SheetFooter className="bg-muted flex-row gap-2 py-2.5">
            <Button
              size="sm"
              className="flex-1"
              onClick={handleGenerateKey}
              disabled={loading}
            >
              {loading ? (
                <Loader2 className="animate-spin size-4 animate-duration-1000" />
              ) : (
                <KeyIcon className="size-4" />
              )}
              {loading ? "Generating..." : "Generate"}
            </Button>
            <Button
              variant="destructive"
              size="sm"
              className="flex-1"
              onClick={() => {
                setOpenGenerateKey(false);
                clearGenStates();
              }}
              disabled={loading}
            >
              <X className="size-4" />
              Cancel
            </Button>
          </SheetFooter>
        </SheetContent>
      </Sheet>

      {/* -------------------- DELETE CONFIRMATION -------------------- */}
      {hostToDelete && (
        <div className="fixed inset-0 z-200 flex items-center justify-center bg-black/50">
          <div className="w-full max-w-sm rounded-lg border bg-background p-5 shadow-lg space-y-4">
            <div className="flex items-center gap-3">
              <div className="size-10 bg-destructive/20 rounded-md flex items-center justify-center shrink-0 border border-destructive/40">
                <TrashIcon className="size-5 text-destructive" />
              </div>
              <div className="space-y-0.5">
                <p className="text-sm font-semibold text-foreground">
                  Delete host
                </p>
                <p className="text-xs text-muted-foreground">
                  This will permanently remove "{hostToDelete.name}". This
                  cannot be undone.
                </p>
              </div>
            </div>
            <div className="flex flex-row gap-2">
              <Button
                variant="destructive"
                className="flex-1"
                onClick={handleDeleteHost}
                disabled={deleting}
              >
                {deleting ? (
                  <Loader2 className="animate-spin size-4" />
                ) : (
                  <TrashIcon className="size-4" />
                )}
                {deleting ? "Deleting..." : "Delete"}
              </Button>
              <Button
                variant="outline"
                className="flex-1"
                onClick={() => setHostToDelete(null)}
                disabled={deleting}
              >
                Cancel
              </Button>
            </div>
          </div>
        </div>
      )}
    </DashboardLayout>
  );
}