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,50 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
|
||||
const api = axios.create({ baseURL: "/" });
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
let refreshing: Promise<string | null> | null = null;
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (error: AxiosError) => {
|
||||
const original = error.config as any;
|
||||
if (error.response?.status === 401 && original && !original._retry) {
|
||||
original._retry = true;
|
||||
if (!refreshing) {
|
||||
refreshing = useAuthStore
|
||||
.getState()
|
||||
.refresh()
|
||||
.finally(() => {
|
||||
refreshing = null;
|
||||
});
|
||||
}
|
||||
const newToken = await refreshing;
|
||||
if (newToken) {
|
||||
original.headers.Authorization = `Bearer ${newToken}`;
|
||||
return api(original);
|
||||
}
|
||||
useAuthStore.getState().logout();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export function apiErrorMessage(err: unknown): string {
|
||||
const e = err as AxiosError<any>;
|
||||
const detail = e?.response?.data?.detail;
|
||||
if (typeof detail === "string") return detail;
|
||||
if (detail?.detail) return `${detail.error}: ${detail.detail}`;
|
||||
if (detail?.error) return detail.error;
|
||||
return e?.message || "Unexpected error";
|
||||
}
|
||||
|
||||
export default api;
|
||||
@@ -0,0 +1,28 @@
|
||||
import api from "./client";
|
||||
import type { StackDetail, StackSummary } from "@/types";
|
||||
|
||||
export const stacksApi = {
|
||||
list: () => api.get<StackSummary[]>("/api/stacks").then((r) => r.data),
|
||||
get: (id: string) =>
|
||||
api.get<StackDetail>(`/api/stacks/${id}`).then((r) => r.data),
|
||||
create: (body: { name: string; description?: string; yaml?: string; env?: string }) =>
|
||||
api.post<StackSummary>("/api/stacks", body).then((r) => r.data),
|
||||
update: (id: string, body: { name?: string; description?: string; yaml?: string; env?: string }) =>
|
||||
api.put<StackSummary>(`/api/stacks/${id}`, body).then((r) => r.data),
|
||||
remove: (id: string, deleteFiles = true) =>
|
||||
api.delete(`/api/stacks/${id}?delete_files=${deleteFiles}`).then((r) => r.data),
|
||||
clone: (id: string, name: string) =>
|
||||
api.post(`/api/stacks/${id}/clone`, { name }).then((r) => r.data),
|
||||
|
||||
start: (id: string) => api.post(`/api/stacks/${id}/start`).then((r) => r.data),
|
||||
stop: (id: string) => api.post(`/api/stacks/${id}/stop`).then((r) => r.data),
|
||||
restart: (id: string) => api.post(`/api/stacks/${id}/restart`).then((r) => r.data),
|
||||
pull: (id: string) => api.post(`/api/stacks/${id}/pull`).then((r) => r.data),
|
||||
update_images: (id: string) => api.post(`/api/stacks/${id}/update`).then((r) => r.data),
|
||||
down: (id: string) => api.post(`/api/stacks/${id}/down`).then((r) => r.data),
|
||||
|
||||
logs: (id: string, tail = 200) =>
|
||||
api.get<{ logs: string }>(`/api/stacks/${id}/logs?tail=${tail}`).then((r) => r.data),
|
||||
convert: (command: string) =>
|
||||
api.post<{ yaml: string }>("/api/stacks/convert", { command }).then((r) => r.data),
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import api from "./client";
|
||||
import type { AuditEntry, SystemInfo } from "@/types";
|
||||
|
||||
export const systemApi = {
|
||||
info: () => api.get<SystemInfo>("/api/system/info").then((r) => r.data),
|
||||
audit: (limit = 10) =>
|
||||
api.get<AuditEntry[]>(`/api/audit?limit=${limit}`).then((r) => r.data),
|
||||
};
|
||||
Reference in New Issue
Block a user