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:
menzelj
2026-06-07 16:04:58 +00:00
co-authored by Claude Opus 4.8
commit f732cb080b
59 changed files with 3677 additions and 0 deletions
+131
View File
@@ -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>
);
}