import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { config } from '../../../config'; import { useAuthStore } from '../../../auth/auth-store'; export interface App { id: string; environmentId: string; slug: string; displayName: string; containerConfig: Record; createdAt: string; updatedAt: string; } export interface AppVersion { id: string; appId: string; version: number; jarPath: string; jarChecksum: string; jarFilename: string; jarSizeBytes: number; detectedRuntimeType: string | null; detectedMainClass: string | null; uploadedAt: string; } export interface Deployment { id: string; appId: string; appVersionId: string; environmentId: string; status: 'STOPPED' | 'STARTING' | 'RUNNING' | 'DEGRADED' | 'STOPPING' | 'FAILED'; targetState: string; deploymentStrategy: string; replicaStates: { index: number; containerId: string; containerName: string; status: string; oomKilled?: boolean }[]; deployStage: string | null; containerId: string | null; containerName: string | null; errorMessage: string | null; deployedAt: string | null; stoppedAt: string | null; createdAt: string; } /** * Authenticated fetch. `path` is relative to apiBaseUrl, must include the * leading slash. All app/deployment endpoints now live under * /api/v1/environments/{envSlug}/... */ async function apiFetch(path: string, options?: RequestInit): Promise { const token = useAuthStore.getState().accessToken; const res = await fetch(`${config.apiBaseUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), 'X-Cameleer-Protocol-Version': '1', ...options?.headers, }, }); if (res.status === 401 || res.status === 403) { useAuthStore.getState().logout(); throw new Error('Unauthorized'); } if (!res.ok) throw new Error(`API error: ${res.status}`); if (res.status === 204) return undefined as T; const text = await res.text(); if (!text) return undefined as T; return JSON.parse(text); } function envBase(envSlug: string): string { return `/environments/${encodeURIComponent(envSlug)}/apps`; } // --- Apps --- export function useApps(envSlug: string | undefined) { return useQuery({ queryKey: ['apps', envSlug], queryFn: () => apiFetch(envBase(envSlug!)), enabled: !!envSlug, }); } export function useCreateApp() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ envSlug, slug, displayName }: { envSlug: string; slug: string; displayName: string }) => apiFetch(envBase(envSlug), { method: 'POST', body: JSON.stringify({ slug, displayName }), }), onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }), }); } export function useDeleteApp() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ envSlug, appSlug }: { envSlug: string; appSlug: string }) => apiFetch(`${envBase(envSlug)}/${encodeURIComponent(appSlug)}`, { method: 'DELETE' }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['apps'] }); qc.invalidateQueries({ queryKey: ['catalog'] }); }, }); } export function useUpdateContainerConfig() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ envSlug, appSlug, config }: { envSlug: string; appSlug: string; config: Record }) => apiFetch(`${envBase(envSlug)}/${encodeURIComponent(appSlug)}/container-config`, { method: 'PUT', body: JSON.stringify(config), }), onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }), }); } // --- Versions --- export function useAppVersions(envSlug: string | undefined, appSlug: string | undefined) { return useQuery({ queryKey: ['apps', envSlug, appSlug, 'versions'], queryFn: () => apiFetch(`${envBase(envSlug!)}/${encodeURIComponent(appSlug!)}/versions`), enabled: !!envSlug && !!appSlug, }); } export function useUploadJar() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ envSlug, appSlug, file }: { envSlug: string; appSlug: string; file: File }) => { const token = useAuthStore.getState().accessToken; const form = new FormData(); form.append('file', file); const res = await fetch( `${config.apiBaseUrl}${envBase(envSlug)}/${encodeURIComponent(appSlug)}/versions`, { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), 'X-Cameleer-Protocol-Version': '1', }, body: form, }); if (!res.ok) throw new Error(`Upload failed: ${res.status}`); return res.json() as Promise; }, onSuccess: (_data, { envSlug, appSlug }) => qc.invalidateQueries({ queryKey: ['apps', envSlug, appSlug, 'versions'] }), }); } // --- Deployments --- export function useDeployments(envSlug: string | undefined, appSlug: string | undefined) { return useQuery({ queryKey: ['apps', envSlug, appSlug, 'deployments'], queryFn: () => apiFetch(`${envBase(envSlug!)}/${encodeURIComponent(appSlug!)}/deployments`), enabled: !!envSlug && !!appSlug, refetchInterval: 5000, }); } export function useCreateDeployment() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ envSlug, appSlug, appVersionId }: { envSlug: string; appSlug: string; appVersionId: string }) => apiFetch( `${envBase(envSlug)}/${encodeURIComponent(appSlug)}/deployments`, { method: 'POST', body: JSON.stringify({ appVersionId }) }, ), onSuccess: (_data, { envSlug, appSlug }) => qc.invalidateQueries({ queryKey: ['apps', envSlug, appSlug, 'deployments'] }), }); } export function useStopDeployment() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ envSlug, appSlug, deploymentId }: { envSlug: string; appSlug: string; deploymentId: string }) => apiFetch( `${envBase(envSlug)}/${encodeURIComponent(appSlug)}/deployments/${deploymentId}/stop`, { method: 'POST' }, ), onSuccess: (_data, { envSlug, appSlug }) => qc.invalidateQueries({ queryKey: ['apps', envSlug, appSlug, 'deployments'] }), }); } export function usePromoteDeployment() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ envSlug, appSlug, deploymentId, targetEnvironment }: { envSlug: string; appSlug: string; deploymentId: string; targetEnvironment: string }) => apiFetch( `${envBase(envSlug)}/${encodeURIComponent(appSlug)}/deployments/${deploymentId}/promote`, { method: 'POST', body: JSON.stringify({ targetEnvironment }) }, ), onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }), }); } // --- Dirty State --- export interface DirtyStateDifference { field: string; staged: string; deployed: string; } export interface DirtyState { dirty: boolean; lastSuccessfulDeploymentId: string | null; differences: DirtyStateDifference[]; } export function useDirtyState(envSlug: string | undefined, appSlug: string | undefined) { return useQuery({ queryKey: ['apps', envSlug, appSlug, 'dirty-state'], queryFn: () => apiFetch( `${envBase(envSlug!)}/${encodeURIComponent(appSlug!)}/dirty-state`, ), enabled: !!envSlug && !!appSlug, }); }