import { useEffect, useMemo, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Play, Square, RotateCw, DownloadCloud, ArrowUpCircle, Power, ArrowLeft, Save, } from "lucide-react"; import { toast } from "sonner"; import { Badge, Button, Card, Spinner, StatusDot } from "@/components/ui"; import { HostDot } from "@/components/hosts/HostDot"; import { LogViewer } from "@/components/stacks/LogViewer"; import { ContainerPorts } from "@/components/stacks/ContainerPorts"; import { BackupButton } from "@/components/stacks/BackupRestore"; import { agentsApi } from "@/api/agents"; import { apiErrorMessage } from "@/api/client"; import { useAuthStore } from "@/store/auth"; const TABS = ["Overview", "Logs", "Environment", "Compose"] as const; type Tab = (typeof TABS)[number]; export function RemoteStackDetail() { const { agentId = "", id = "" } = useParams(); const aid = Number(agentId); const qc = useQueryClient(); const isAdmin = useAuthStore((s) => s.user?.role === "admin"); const [tab, setTab] = useState("Overview"); const [busy, setBusy] = useState(false); const { data, isLoading } = useQuery({ queryKey: ["agent-stack", aid, id], queryFn: () => agentsApi.stack(aid, id), refetchInterval: 5000, }); // Port links on a remote stack must target the agent host, not this host. const agents = useQuery({ queryKey: ["agents"], queryFn: () => agentsApi.list(false) }); const agentHost = useMemo(() => { const a = agents.data?.find((x) => x.id === aid); if (!a) return undefined; try { return new URL(a.url).hostname; } catch { return undefined; } }, [agents.data, aid]); const run = async (action: string, label: string) => { setBusy(true); const t = toast.loading(`${label} ${id}…`); try { await agentsApi.action(aid, id, action); toast.success(`${label} ${id} ✓`, { id: t }); qc.invalidateQueries({ queryKey: ["agent-stack", aid, id] }); } catch (e) { toast.error(apiErrorMessage(e), { id: t }); } finally { setBusy(false); } }; if (isLoading || !data) return ; return (
All stacks

{data.name}

{data.status}

on {data.agent_name}

{isAdmin && (
)}
{TABS.map((t) => ( ))}
{tab === "Overview" && } {tab === "Logs" && ( )} {tab === "Environment" && ( )} {tab === "Compose" && ( )}
); } function Overview({ containers, host }: { containers: any[]; host?: string }) { return (
{containers.length === 0 && (

No containers running.

)} {containers.map((c) => (

{c.service}

{c.image}

{c.health && {c.health}} {c.status}
))}
); } function RemoteEditor({ agentId, stackId, field, value, canEdit, queryKey, }: { agentId: number; stackId: string; field: "yaml" | "env"; value: string; canEdit: boolean; queryKey: unknown[]; }) { const qc = useQueryClient(); const [text, setText] = useState(value); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); useEffect(() => { if (!editing) setText(value); }, [value, editing]); const save = async () => { setSaving(true); try { const body = field === "yaml" ? { yaml: text } : { env: text }; await agentsApi.update_stack(agentId, stackId, body); toast.success("Saved. Restart or update the stack to apply."); setEditing(false); qc.invalidateQueries({ queryKey }); } catch (e) { toast.error(apiErrorMessage(e)); } finally { setSaving(false); } }; if (!editing) { return ( {canEdit && (
)} {value ? (
{value}
) : (

{field === "env" ? "No .env file for this stack." : "Empty compose file."}

)}
); } return (