Phase 5: multi-host agents (0.5.0)
- stackpilot-agent: slim token-guarded FastAPI (reuses compose_service) exposing stack CRUD/lifecycle/logs + system info; same image, different CMD. agent/ Dockerfile + compose + .env.example. - Central proxy: Agent model, agent_service (httpx ping/proxy + live status: online/offline/unauthorized + hostname/last_seen), routers/agents.py (CRUD + ping + proxied stacks/lifecycle/logs/system). - Frontend: Settings → Remote hosts (add/check/remove, connectivity dot); Stacks grouped by host; remote stack detail with lifecycle, live logs, compose/.env edit. Verified end-to-end: agent+main on a shared network — register (good/bad token), list/create/start/logs/delete remote stacks, offline detection (502). Remote backup destinations (SFTP/S3) deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8d19b09abd
commit
59037f4287
@@ -1,11 +1,13 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { Plus, Search, HardDrive } from "lucide-react";
|
||||
import { Button, Input, Spinner, Card } from "@/components/ui";
|
||||
import { StackCard } from "@/components/stacks/StackCard";
|
||||
import { RestoreButton } from "@/components/stacks/BackupRestore";
|
||||
import { AgentStacksSection } from "@/components/stacks/AgentStacksSection";
|
||||
import { stacksApi } from "@/api/stacks";
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import { useStackActions } from "@/hooks/useStackActions";
|
||||
|
||||
@@ -23,6 +25,13 @@ export function Stacks() {
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const agents = useQuery({
|
||||
queryKey: ["agents"],
|
||||
queryFn: () => agentsApi.list(),
|
||||
refetchInterval: 15000,
|
||||
});
|
||||
const hasAgents = (agents.data?.length ?? 0) > 0;
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let list = (data ?? []).filter(
|
||||
(s) =>
|
||||
@@ -68,27 +77,38 @@ export function Stacks() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : filtered.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{filtered.map((s) => (
|
||||
<StackCard
|
||||
key={s.id}
|
||||
stack={s}
|
||||
isAdmin={isAdmin}
|
||||
busy={busyId === s.id}
|
||||
onStart={start}
|
||||
onStop={stop}
|
||||
onRestart={restart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<p className="text-sm text-slate-500">No stacks match your search.</p>
|
||||
</Card>
|
||||
)}
|
||||
<section>
|
||||
{hasAgents && (
|
||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-slate-500">
|
||||
<HardDrive className="h-4 w-4" /> This host
|
||||
</h2>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : filtered.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{filtered.map((s) => (
|
||||
<StackCard
|
||||
key={s.id}
|
||||
stack={s}
|
||||
isAdmin={isAdmin}
|
||||
busy={busyId === s.id}
|
||||
onStart={start}
|
||||
onStop={stop}
|
||||
onRestart={restart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<p className="text-sm text-slate-500">No stacks match your search.</p>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{agents.data?.map((agent) => (
|
||||
<AgentStacksSection key={agent.id} agent={agent} isAdmin={isAdmin} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user