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:
menzeljandClaude Opus 4.8 committed 2026-06-07 16:04:58 +00:00
commit f732cb080b
59 files changed
+3677

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
import axios from "axios";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { TokenPair, User } from "@/types";
interface AuthState {
accessToken: string | null;
refreshToken: string | null;
user: User | null;
setTokens: (t: TokenPair) => void;
login: (username: string, password: string) => Promise<void>;
setup: (username: string, password: string) => Promise<void>;
refresh: () => Promise<string | null>;
fetchMe: () => Promise<void>;
logout: () => void;
}
// Raw client without interceptors (avoids refresh loops).
const raw = axios.create({ baseURL: "/" });
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
accessToken: null,
refreshToken: null,
user: null,
setTokens: (t) =>
set({ accessToken: t.access_token, refreshToken: t.refresh_token }),
login: async (username, password) => {
const { data } = await raw.post<TokenPair>("/api/auth/login", {
username,
password,
});
set({ accessToken: data.access_token, refreshToken: data.refresh_token });
await get().fetchMe();
},
setup: async (username, password) => {
const { data } = await raw.post<TokenPair>("/api/auth/setup", {
username,
password,
role: "admin",
});
set({ accessToken: data.access_token, refreshToken: data.refresh_token });
await get().fetchMe();
},
refresh: async () => {
const rt = get().refreshToken;
if (!rt) return null;
try {
const { data } = await raw.post<TokenPair>("/api/auth/refresh", {
refresh_token: rt,
});
set({ accessToken: data.access_token, refreshToken: data.refresh_token });
return data.access_token;
} catch {
set({ accessToken: null, refreshToken: null, user: null });
return null;
}
},
fetchMe: async () => {
const token = get().accessToken;
if (!token) return;
const { data } = await raw.get<User>("/api/auth/me", {
headers: { Authorization: `Bearer ${token}` },
});
set({ user: data });
},
logout: () => set({ accessToken: null, refreshToken: null, user: null }),
}),
{ name: "stackpilot-auth" }
)
);
+26
View File
@@ -0,0 +1,26 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface ThemeState {
theme: "dark" | "light";
toggle: () => void;
apply: () => void;
}
export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: "dark",
toggle: () => {
set({ theme: get().theme === "dark" ? "light" : "dark" });
get().apply();
},
apply: () => {
const root = document.documentElement;
if (get().theme === "dark") root.classList.add("dark");
else root.classList.remove("dark");
},
}),
{ name: "stackpilot-theme" }
)
);