Initial commit: StackPilot Phase 1 (Core)
Self-hosted Docker Compose manager. - Backend: FastAPI + docker-py + SQLite (JWT auth, file-first stacks, lifecycle, live status, WebSocket logs, docker-run converter, audit log) - Frontend: React + Vite + Tailwind (login/setup, dashboard, stacks, stack detail, Monaco editor, dark/light theme) - Deployment: docker-compose.yml, Dockerfiles, nginx reverse proxy Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Cpu, MemoryStick, HardDrive, Container, Clock } from "lucide-react";
|
||||
import { Card, Spinner } from "@/components/ui";
|
||||
import { StackCard } from "@/components/stacks/StackCard";
|
||||
import { stacksApi } from "@/api/stacks";
|
||||
import { systemApi } from "@/api/system";
|
||||
import { formatBytes, formatUptime, relativeTime } from "@/lib/utils";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import { useStackActions } from "@/hooks/useStackActions";
|
||||
|
||||
export function Dashboard() {
|
||||
const isAdmin = useAuthStore((s) => s.user?.role === "admin");
|
||||
const { busyId, start, stop, restart } = useStackActions();
|
||||
|
||||
const stacks = useQuery({ queryKey: ["stacks"], queryFn: stacksApi.list, refetchInterval: 5000 });
|
||||
const info = useQuery({ queryKey: ["system"], queryFn: systemApi.info, refetchInterval: 5000 });
|
||||
const audit = useQuery({ queryKey: ["audit"], queryFn: () => systemApi.audit(10), refetchInterval: 10000 });
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Resource bar */}
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<Stat icon={<Cpu className="h-5 w-5" />} label="CPU cores" value={info.data?.cpu_cores ?? "—"} />
|
||||
<Stat
|
||||
icon={<MemoryStick className="h-5 w-5" />}
|
||||
label="Memory"
|
||||
value={
|
||||
info.data
|
||||
? `${formatBytes(info.data.ram.used)} / ${formatBytes(info.data.ram.total)}`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
<Stat
|
||||
icon={<Container className="h-5 w-5" />}
|
||||
label="Containers"
|
||||
value={
|
||||
info.data
|
||||
? `${info.data.containers_running} / ${info.data.containers_total}`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
<Stat
|
||||
icon={<HardDrive className="h-5 w-5" />}
|
||||
label="Docker"
|
||||
value={info.data?.docker_version ?? "—"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stacks grid */}
|
||||
<section>
|
||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-500">
|
||||
Stacks
|
||||
</h2>
|
||||
{stacks.isLoading ? (
|
||||
<Spinner />
|
||||
) : stacks.data && stacks.data.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{stacks.data.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 yet. Create one from the Stacks page.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Recent activity */}
|
||||
<section>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-slate-500">
|
||||
<Clock className="h-4 w-4" /> Recent activity
|
||||
</h2>
|
||||
<Card>
|
||||
{audit.data && audit.data.length > 0 ? (
|
||||
<ul className="divide-y divide-slate-100 text-sm dark:divide-slate-700">
|
||||
{audit.data.map((a) => (
|
||||
<li key={a.id} className="flex items-center justify-between py-2">
|
||||
<span>
|
||||
<span className="font-medium">{a.user}</span>{" "}
|
||||
<span className="text-slate-500">{a.action}</span>{" "}
|
||||
<span className="font-mono text-xs text-accent dark:text-accent-dark">
|
||||
{a.target}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
{relativeTime(a.timestamp)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">No activity yet.</p>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Card className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-accent/10 p-2 text-accent dark:bg-accent-dark/10 dark:text-accent-dark">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-slate-500">{label}</p>
|
||||
<p className="truncate text-sm font-semibold">{value}</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user