diff --git a/backend/agent_app.py b/backend/agent_app.py index f0c9e8d..cecdd55 100644 --- a/backend/agent_app.py +++ b/backend/agent_app.py @@ -62,7 +62,7 @@ def _map_docker(exc: DockerError): raise HTTPException(status_code=code, detail=exc.detail or exc.error) raise exc # falls through to the global 502 DockerError handler -AGENT_VERSION = "0.21.1" +AGENT_VERSION = "0.21.3" # --------------------------------------------------------------------------- # diff --git a/backend/main.py b/backend/main.py index d14f098..163cfd4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,7 +55,7 @@ async def lifespan(app: FastAPI): schedule_task.cancel() -app = FastAPI(title="StackPilot", version="0.21.2", lifespan=lifespan) +app = FastAPI(title="StackPilot", version="0.21.3", lifespan=lifespan) app.add_middleware( CORSMiddleware, diff --git a/frontend/package.json b/frontend/package.json index 1de8c36..23ba9f4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.21.1", + "version": "0.21.3", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/stacks/StacksTable.tsx b/frontend/src/components/stacks/StacksTable.tsx new file mode 100644 index 0000000..c0821bd --- /dev/null +++ b/frontend/src/components/stacks/StacksTable.tsx @@ -0,0 +1,231 @@ +import { Link } from "react-router-dom"; +import { Play, Square, RotateCw } from "lucide-react"; +import { Card, Spinner, StatusDot, Badge } from "@/components/ui"; +import { formatBytes } from "@/lib/utils"; +import type { StackStats, StackSummary } from "@/types"; + +/** Stack list rendered as a table with live CPU/memory usage meters and inline + * start/stop/restart, shared by the Dashboard and the Stacks page. */ +export function StacksTable({ + stacks, + stats, + hostCpus, + hostMem, + isAdmin, + busyId, + loading, + linkBase = "/stacks", + onStart, + onStop, + onRestart, + emptyText, +}: { + stacks: StackSummary[] | undefined; + stats: Record | undefined; + hostCpus: number; + hostMem: number; + isAdmin: boolean; + busyId: string | null; + loading: boolean; + linkBase?: string; + onStart: (id: string) => void; + onStop: (id: string) => void; + onRestart: (id: string) => void; + emptyText: string; +}) { + if (loading) return ; + if (!stacks || stacks.length === 0) { + return ( + +

{emptyText}

+
+ ); + } + return ( + + + + + + + + {isAdmin && } + + + + {stacks.map((s) => ( + + ))} + +
StackCPUMemory
+
+ ); +} + +function StackRow({ + stack, + stats, + hostCpus, + hostMem, + isAdmin, + busy, + linkBase, + onStart, + onStop, + onRestart, +}: { + stack: StackSummary; + stats?: StackStats; + hostCpus: number; + hostMem: number; + isAdmin: boolean; + busy: boolean; + linkBase: string; + onStart: (id: string) => void; + onStop: (id: string) => void; + onRestart: (id: string) => void; +}) { + const running = stack.running_count > 0; + + return ( + + + + + {stack.name} + {stack.status} + + {stack.running_count}/{stack.service_count} svc + + + + + {running && stats ? ( + + ) : ( + + )} + + + {running && stats ? ( + + ) : ( + + )} + + {isAdmin && ( + +
+ {running ? ( + <> + onRestart(stack.id)} disabled={busy}> + + + onStop(stack.id)} disabled={busy}> + + + + ) : ( + onStart(stack.id)} disabled={busy}> + + + )} +
+ + )} + + ); +} + +function IconBtn({ + title, + onClick, + disabled, + children, +}: { + title: string; + onClick: () => void; + disabled?: boolean; + children: React.ReactNode; +}) { + return ( + + ); +} + +/** A compact usage bar. When a limit is set the bar fills toward the limit; + * otherwise it fills toward the host total as a faint reference. */ +function Meter({ + used, + limit, + hostMax, + label, +}: { + used: number; + limit: number | null; + hostMax: number; + label: string; +}) { + const denom = limit ?? (hostMax || 0); + const pct = denom > 0 ? Math.min((used / denom) * 100, 100) : 0; + const over = limit != null && used > limit * 1.001; + const bar = + over || pct >= 90 + ? "bg-red-500" + : pct >= 75 + ? "bg-amber-500" + : limit != null + ? "bg-sky-500" + : "bg-slate-400"; + + return ( +
+
+ {label} + {denom > 0 && ( + {Math.round(pct)}% + )} +
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 22a6aae..33d2ea5 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -8,15 +8,13 @@ import { Container, Clock, ArrowUpCircle, - Play, - Square, - RotateCw, Server, Database, } from "lucide-react"; import { toast } from "sonner"; -import { Card, Spinner, StatusDot, Badge } from "@/components/ui"; +import { Card } from "@/components/ui"; import { HostHeader } from "@/components/hosts/HostHeader"; +import { StacksTable } from "@/components/stacks/StacksTable"; import { stacksApi } from "@/api/stacks"; import { systemApi } from "@/api/system"; import { imagesApi } from "@/api/images"; @@ -26,7 +24,7 @@ import { apiErrorMessage } from "@/api/client"; import { formatBytes, relativeTime } from "@/lib/utils"; import { useAuthStore } from "@/store/auth"; import { useStackActions } from "@/hooks/useStackActions"; -import type { Agent, StackStats, StackSummary } from "@/types"; +import type { Agent } from "@/types"; const sumSizes = (m: Record) => Object.values(m).reduce((a, b) => a + (b ?? 0), 0); @@ -216,230 +214,6 @@ function AgentDashboardSection({ agent, isAdmin }: { agent: Agent; isAdmin: bool ); } -function StacksTable({ - stacks, - stats, - hostCpus, - hostMem, - isAdmin, - busyId, - loading, - linkBase = "/stacks", - onStart, - onStop, - onRestart, - emptyText, -}: { - stacks: StackSummary[] | undefined; - stats: Record | undefined; - hostCpus: number; - hostMem: number; - isAdmin: boolean; - busyId: string | null; - loading: boolean; - linkBase?: string; - onStart: (id: string) => void; - onStop: (id: string) => void; - onRestart: (id: string) => void; - emptyText: string; -}) { - if (loading) return ; - if (!stacks || stacks.length === 0) { - return ( - -

{emptyText}

-
- ); - } - return ( - - - - - - - - {isAdmin && } - - - - {stacks.map((s) => ( - - ))} - -
StackCPUMemory
-
- ); -} - -function StackRow({ - stack, - stats, - hostCpus, - hostMem, - isAdmin, - busy, - linkBase, - onStart, - onStop, - onRestart, -}: { - stack: StackSummary; - stats?: StackStats; - hostCpus: number; - hostMem: number; - isAdmin: boolean; - busy: boolean; - linkBase: string; - onStart: (id: string) => void; - onStop: (id: string) => void; - onRestart: (id: string) => void; -}) { - const running = stack.running_count > 0; - - return ( - - - - - {stack.name} - {stack.status} - - {stack.running_count}/{stack.service_count} svc - - - - - {running && stats ? ( - - ) : ( - - )} - - - {running && stats ? ( - - ) : ( - - )} - - {isAdmin && ( - -
- {running ? ( - <> - onRestart(stack.id)} disabled={busy}> - - - onStop(stack.id)} disabled={busy}> - - - - ) : ( - onStart(stack.id)} disabled={busy}> - - - )} -
- - )} - - ); -} - -function IconBtn({ - title, - onClick, - disabled, - children, -}: { - title: string; - onClick: () => void; - disabled?: boolean; - children: React.ReactNode; -}) { - return ( - - ); -} - -/** A compact usage bar. When a limit is set the bar fills toward the limit; - * otherwise it fills toward the host total as a faint reference. */ -function Meter({ - used, - limit, - hostMax, - label, -}: { - used: number; - limit: number | null; - hostMax: number; - label: string; -}) { - const denom = limit ?? (hostMax || 0); - const pct = denom > 0 ? Math.min((used / denom) * 100, 100) : 0; - const over = limit != null && used > limit * 1.001; - const bar = - over || pct >= 90 - ? "bg-red-500" - : pct >= 75 - ? "bg-amber-500" - : limit != null - ? "bg-sky-500" - : "bg-slate-400"; - - return ( -
-
- {label} - {denom > 0 && ( - {Math.round(pct)}% - )} -
-
-
-
-
- ); -} - function ResourceBar({ cpuCores, memUsed,