import type { ApiEnvelope, ContainerStats, Deploy, DeployLog, DockerHealth, ProxyHealth, EventLogEntry, EventLogStats, InspectResult, Instance, LocalImage, NpmCertificate, NpmAccessList, ProxyRoute, Project, ProjectDetail, Registry, RegistryImage, Settings, StaleContainer, Stage, StageEnv, Volume, VolumeScopeInfo, BrowseResult, DnsZone, DnsRecordView, BackupInfo } from './types'; // ── Helpers ───────────────────────────────────────────────────────── class ApiError extends Error { constructor( message: string, public readonly status: number ) { super(message); this.name = 'ApiError'; } } import { getAuthToken, clearAuth } from './auth'; async function request(path: string, init?: RequestInit): Promise { const token = getAuthToken(); const headers: Record = { 'Content-Type': 'application/json', ...(init?.headers as Record) }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const res = await fetch(path, { ...init, headers }); // Redirect to login on 401 (expired/missing token). if (res.status === 401 && typeof window !== 'undefined' && !path.includes('/auth/')) { clearAuth(); window.location.href = '/login'; throw new ApiError('Authentication required', 401); } let envelope: ApiEnvelope; try { envelope = await res.json(); } catch { throw new ApiError( `Server returned non-JSON response (HTTP ${res.status})`, res.status ); } if (!envelope.success) { throw new ApiError(envelope.error ?? 'Unknown API error', res.status); } return envelope.data as T; } function get(path: string): Promise { return request(path); } function post(path: string, body?: unknown): Promise { return request(path, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }); } function put(path: string, body: unknown): Promise { return request(path, { method: 'PUT', body: JSON.stringify(body) }); } function del(path: string): Promise { return request(path, { method: 'DELETE' }); } // ── Projects ──────────────────────────────────────────────────────── export function listProjects(): Promise { return get('/api/projects'); } export function getProject(id: string): Promise { return get(`/api/projects/${id}`); } export function createProject(data: Partial): Promise { return post('/api/projects', data); } export function updateProject(id: string, data: Partial): Promise { return put(`/api/projects/${id}`, data); } export function deleteProject(id: string): Promise<{ deleted: string }> { return del<{ deleted: string }>(`/api/projects/${id}`); } // ── Stages ───────────────────────────────────────────────────────── export function createStage(projectId: string, data: Partial): Promise { return post(`/api/projects/${projectId}/stages`, data); } export function updateStage(projectId: string, stageId: string, data: Partial): Promise { return put(`/api/projects/${projectId}/stages/${stageId}`, data); } export function deleteStage(projectId: string, stageId: string): Promise { return del(`/api/projects/${projectId}/stages/${stageId}`); } // ── Instances ─────────────────────────────────────────────────────── export function listInstances(projectId: string, stageId: string): Promise { return get(`/api/projects/${projectId}/stages/${stageId}/instances`); } export function deployInstance( projectId: string, stageId: string, imageTag: string ): Promise<{ status: string }> { return post<{ status: string }>(`/api/projects/${projectId}/stages/${stageId}/instances`, { image_tag: imageTag }); } export function removeInstance( projectId: string, stageId: string, instanceId: string ): Promise<{ deleted: string }> { return del<{ deleted: string }>( `/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}` ); } export function stopInstance( projectId: string, stageId: string, instanceId: string ): Promise<{ status: string }> { return post<{ status: string }>( `/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stop` ); } export function startInstance( projectId: string, stageId: string, instanceId: string ): Promise<{ status: string }> { return post<{ status: string }>( `/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/start` ); } export function restartInstance( projectId: string, stageId: string, instanceId: string ): Promise<{ status: string }> { return post<{ status: string }>( `/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/restart` ); } // ── Deploys ───────────────────────────────────────────────────────── export function listDeploys(limit = 50): Promise { return get(`/api/deploys?limit=${limit}`); } export function getDeployLogs(deployId: string): Promise { return get(`/api/deploys/${deployId}/logs`); } export function inspectImage(image: string): Promise { return post('/api/deploy/inspect', { image }); } export function quickDeploy(data: { name?: string; image: string; tag?: string; registry?: string; port?: number; force?: boolean; enable_proxy?: boolean; auto_deploy?: boolean; }): Promise<{ project: Project; status: string }> { return post<{ project: Project; status: string }>('/api/deploy/quick', data); } // ── Registries ────────────────────────────────────────────────────── export function listRegistries(): Promise { return get('/api/registries'); } export function createRegistry(data: Partial): Promise { return post('/api/registries', data); } export function updateRegistry(id: string, data: Partial): Promise { return put(`/api/registries/${id}`, data); } export function deleteRegistry(id: string): Promise<{ deleted: string }> { return del<{ deleted: string }>(`/api/registries/${id}`); } export function testRegistry(id: string): Promise<{ status: string }> { return post<{ status: string }>(`/api/registries/${id}/test`); } export function listRegistryTags(registryId: string, image: string): Promise { return get(`/api/registries/${registryId}/tags/${encodeURIComponent(image)}`); } export function listRegistryImages(registryId: string): Promise { return get(`/api/registries/${registryId}/images`); } // ── Settings ──────────────────────────────────────────────────────── export function getSettings(): Promise { return get('/api/settings'); } export function updateSettings(data: Partial): Promise { return put('/api/settings', data); } export function getWebhookUrl(): Promise<{ webhook_url: string }> { return get<{ webhook_url: string }>('/api/settings/webhook-url'); } export function regenerateWebhookUrl(): Promise<{ webhook_url: string }> { return post<{ webhook_url: string }>('/api/settings/webhook-url/regenerate'); } // ── Proxy Routes ─────────────────────────────────────────────────── export function listProxyRoutes(): Promise { return get('/api/proxies'); } // ── Docker Management ────────────────────────────────────────────── export function fetchContainerLogs( projectId: string, stageId: string, instanceId: string, tail = 200 ): Promise { return get(`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/logs?tail=${tail}`); } export function listProjectImages(projectId: string): Promise { return get(`/api/projects/${projectId}/images`); } export function pruneImages(): Promise<{ images_removed: number; space_reclaimed_mb: number }> { return post<{ images_removed: number; space_reclaimed_mb: number }>('/api/docker/prune-images'); } export function testNpmConnection(data: { npm_url?: string; npm_email?: string; npm_password?: string }): Promise<{ status: string }> { return post<{ status: string }>('/api/settings/npm/test', data); } export function listNpmCertificates(): Promise { return get('/api/settings/npm-certificates'); } export function listNpmAccessLists(): Promise { return get('/api/settings/npm-access-lists'); } // ── DNS ──────────────────────────────────────────────────────────── export function testDnsConnection(provider: string, token: string, zoneId: string): Promise<{ success: boolean; error?: string }> { return post<{ success: boolean; error?: string }>('/api/settings/dns/test', { provider, token, zone_id: zoneId }); } export function listDnsZones(token?: string): Promise { return post('/api/settings/dns/zones', { token: token ?? '' }); } export function getDnsRecords(): Promise { return get('/api/dns/records'); } export function syncDnsRecords(): Promise<{ created: number; deleted: number; already_synced: number }> { return post<{ created: number; deleted: number; already_synced: number }>('/api/dns/sync'); } export function deleteDnsRecord(fqdn: string): Promise { return del(`/api/dns/records/${encodeURIComponent(fqdn)}`); } // ── Backups ──────────────────────────────────────────────────────── export function listBackups(): Promise { return get('/api/backups'); } export function triggerBackup(): Promise { return post('/api/backups'); } export function deleteBackup(id: string): Promise { return del(`/api/backups/${id}`); } export function restoreBackup(id: string): Promise<{ status: string; message: string }> { return post<{ status: string; message: string }>(`/api/backups/${id}/restore`); } export function backupDownloadUrl(id: string): string { return `/api/backups/${id}/download`; } // ── Health ────────────────────────────────────────────────────────── export function getHealth(): Promise<{ docker: DockerHealth; proxy?: ProxyHealth }> { return get<{ docker: DockerHealth; proxy?: ProxyHealth }>('/api/health'); } // ── Auth ───────────────────────────────────────────────────────────── export function login(username: string, password: string): Promise<{ token: string; expires_at: string }> { return post<{ token: string; expires_at: string }>('/api/auth/login', { username, password }); } export function getCurrentUser(): Promise<{ id: string; username: string; email: string; role: string }> { return get<{ id: string; username: string; email: string; role: string }>('/api/auth/me'); } // Auth settings export async function getAuthSettings(): Promise { return request('/api/auth/settings'); } export async function updateAuthSettings(settings: any): Promise { return request('/api/auth/settings', { method: 'PUT', body: JSON.stringify(settings) }); } export async function listUsers(): Promise { return request('/api/auth/users'); } export async function createUser(data: { username: string; password: string; email?: string; role?: string }): Promise { return request('/api/auth/users', { method: 'POST', body: JSON.stringify(data) }); } export async function updateUser(uid: string, data: { email?: string; role?: string }): Promise { return request(`/api/auth/users/${uid}`, { method: 'PUT', body: JSON.stringify(data) }); } export async function changeUserPassword(uid: string, password: string): Promise { return request(`/api/auth/users/${uid}/password`, { method: 'PUT', body: JSON.stringify({ password }) }); } export async function deleteUser(uid: string): Promise { return request(`/api/auth/users/${uid}`, { method: 'DELETE' }); } export async function logout(): Promise { await request('/api/auth/logout', { method: 'POST' }); } // ── Config Export ──────────────────────────────────────────────────── export function exportConfigUrl(): string { return '/api/config/export'; } // ── Stage Env Overrides ────────────────────────────────────────────── export function listStageEnv(projectId: string, stageId: string): Promise { return get(`/api/projects/${projectId}/stages/${stageId}/env`); } export function createStageEnv( projectId: string, stageId: string, data: { key: string; value: string; encrypted?: boolean } ): Promise { return post(`/api/projects/${projectId}/stages/${stageId}/env`, data); } export function updateStageEnv( projectId: string, stageId: string, envId: string, data: { key?: string; value?: string; encrypted?: boolean } ): Promise { return put(`/api/projects/${projectId}/stages/${stageId}/env/${envId}`, data); } export function deleteStageEnv( projectId: string, stageId: string, envId: string ): Promise<{ deleted: string }> { return del<{ deleted: string }>(`/api/projects/${projectId}/stages/${stageId}/env/${envId}`); } // ── Volumes ────────────────────────────────────────────────────────── export function listVolumes(projectId: string): Promise { return get(`/api/projects/${projectId}/volumes`); } export function createVolume( projectId: string, data: { source: string; target: string; scope: string; name?: string; mode?: string } ): Promise { return post(`/api/projects/${projectId}/volumes`, data); } export function updateVolume( projectId: string, volId: string, data: { source?: string; target?: string; scope?: string; name?: string; mode?: string } ): Promise { return put(`/api/projects/${projectId}/volumes/${volId}`, data); } export function listVolumeScopes(): Promise { return get('/api/volumes/scopes'); } export function deleteVolume( projectId: string, volId: string ): Promise<{ deleted: string }> { return del<{ deleted: string }>(`/api/projects/${projectId}/volumes/${volId}`); } export function browseVolume( projectId: string, volId: string, params?: { path?: string; stage?: string; tag?: string } ): Promise { const query = new URLSearchParams(); if (params?.path) query.set('path', params.path); if (params?.stage) query.set('stage', params.stage); if (params?.tag) query.set('tag', params.tag); const qs = query.toString(); return get(`/api/projects/${projectId}/volumes/${volId}/browse${qs ? `?${qs}` : ''}`); } export function volumeDownloadUrl( projectId: string, volId: string, params?: { path?: string; stage?: string; tag?: string } ): string { const query = new URLSearchParams(); if (params?.path) query.set('path', params.path); if (params?.stage) query.set('stage', params.stage); if (params?.tag) query.set('tag', params.tag); const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null; if (token) query.set('token', token); const qs = query.toString(); return `/api/projects/${projectId}/volumes/${volId}/download${qs ? `?${qs}` : ''}`; } export async function uploadToVolume( projectId: string, volId: string, files: FileList, params?: { path?: string; stage?: string; tag?: string } ): Promise<{ uploaded: string[]; count: number }> { const query = new URLSearchParams(); if (params?.path) query.set('path', params.path); if (params?.stage) query.set('stage', params.stage); if (params?.tag) query.set('tag', params.tag); const qs = query.toString(); const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null; const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`/api/projects/${projectId}/volumes/${volId}/upload${qs ? `?${qs}` : ''}`, { method: 'POST', headers, body: formData, }); const envelope = await res.json(); if (!envelope.success) throw new Error(envelope.error ?? 'Upload failed'); return envelope.data; } // ── Event Log ─────────────────────────────────────────────────────── export function fetchEventLog(params?: { severity?: string; source?: string; since?: string; until?: string; limit?: number; offset?: number; }): Promise { const query = new URLSearchParams(); if (params?.severity) query.set('severity', params.severity); if (params?.source) query.set('source', params.source); if (params?.since) query.set('since', params.since); if (params?.until) query.set('until', params.until); if (params?.limit) query.set('limit', String(params.limit)); if (params?.offset) query.set('offset', String(params.offset)); const qs = query.toString(); return get(`/api/events/log${qs ? `?${qs}` : ''}`); } export function fetchEventLogStats(): Promise { return get('/api/events/log/stats'); } export function deleteEvent(id: number): Promise<{ status: string }> { return del<{ status: string }>(`/api/events/log/${id}`); } export function clearAllEvents(): Promise<{ status: string; count: number }> { return del<{ status: string; count: number }>('/api/events/log'); } // ── Stale Containers ──────────────────────────────────────────────── export function fetchStaleContainers(): Promise { return get('/api/containers/stale'); } export function cleanupStaleContainer(id: string): Promise<{ deleted: string }> { return post<{ deleted: string }>(`/api/containers/stale/${id}/cleanup`); } export function bulkCleanupStaleContainers(): Promise<{ deleted: number }> { return post<{ deleted: number }>('/api/containers/stale/cleanup'); } // ── Container Stats ──────────────────────────────────────────────── export function fetchContainerStats( projectId: string, stageId: string, instanceId: string ): Promise { return get( `/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stats` ); } export { ApiError };