import type { components, paths } from '@/generated/api-types'; export type ServiceStatus = 'running' | 'stopped' | 'building' | 'failed' | 'unknown'; export type ProjectStats = components['schemas']['ProjectStats']; export type ProjectEntity = { id: string; name: string; description: string; createdAt?: string; updatedAt?: string; stats: ProjectStats; }; export type UserProfile = { id: string; email: string; name: string; avatarUrl?: string; createdAt?: string; updatedAt?: string; }; export type ServiceEntity = { id: string; projectId: string; name: string; type: string; status: ServiceStatus; createdAt?: string; updatedAt?: string; image?: string; command?: string; environment?: string; gitRepo?: string; gitBranch?: string; buildPath?: string; cpu?: string; memory?: string; }; export type ServiceVariable = { id: string; serviceId: string; key: string; value: string; isSecret: boolean; createdAt?: string; updatedAt?: string; }; export type BuildStatus = 'pending' | 'running' | 'success' | 'failed' | 'cancelled'; export type BuildEntity = { id: string; projectId?: string; serviceId?: string; status: BuildStatus; progress: number; startedAt?: string; completedAt?: string; imageName?: string; imageTag?: string; size: number; error?: string; log?: string; metadata: Record; }; export type ListBuildsInput = { projectId?: string; serviceId?: string; status?: BuildStatus; page?: number; limit?: number; }; export type ListBuildsResult = { builds: BuildEntity[]; total: number; page: number; limit: number; }; export type TemplateEntity = { id: string; name: string; description: string; category: string; logo: string; configRaw: string; variablesRaw: string; isOfficial: boolean; createdAt?: string; updatedAt?: string; }; export type TemplateConfigEntity = { type: string; runtime: string; buildCommand: string; startCommand: string; port: number; healthCheck: string; environment: Record; dockerfile?: string; nixpacksConfig: Record; }; export type TemplateVariableEntity = { key: string; label: string; defaultValue: string; required: boolean; secret: boolean; description: string; }; export type TemplateDetailEntity = { template: TemplateEntity; config: TemplateConfigEntity; variables: TemplateVariableEntity[]; }; export type ListTemplatesInput = { category?: string; }; export type DeployTemplateInput = { projectId: string; name: string; variables?: Record; }; export type DeployTemplateResult = { serviceId: string; message: string; }; export type UpdateUserProfileInput = { name?: string; avatarUrl?: string; }; export type CreateUserInput = components['schemas']['ManualUserCreateRequest']; export type UpgradeStatus = { imageRef: string; registry: string; dockerAvailable: boolean; installed: boolean; digest?: string; size?: number; authConfigured: boolean; message: string; }; export type SystemLoad = { load1m: number; load5m: number; load15m: number; }; export type HostMonitoring = { hostname: string; os: string; architecture: string; cpu: { cores: number; }; memory: { total: number; used: number; available: number; usagePercent: number; }; storage: { path: string; total: number; used: number; available: number; usagePercent: number; }; load: SystemLoad; uptimeSeconds: number; dockerAvailable: boolean; docker?: { containers?: number; images?: number; driver?: string; server?: string; }; collectedAt: string; }; export type AgentCapabilities = { containerRuntimes: string[]; supportedArchitectures: string[]; maxContainers: number; storageDriver: string; networkPlugins: string[]; features: string[]; }; export type NodeResources = { cpu: { cores: number; allocation: number; usage: number; }; memory: { total: number; allocated: number; used: number; available: number; }; storage: { total: number; allocated: number; used: number; available: number; }; }; export type NodeAgentEntity = { id: string; name: string; hostname: string; ipAddress: string; port: number; status: string; version: string; capabilities: AgentCapabilities; resources: NodeResources; lastHeartbeat?: string; createdAt?: string; updatedAt?: string; }; export type AuditLogEntity = { id: string; userId: string; userEmail: string; resource: string; resourceId: string; action: string; details: string; ipAddress: string; userAgent: string; createdAt?: string; }; export type ListAuditLogsInput = { resource?: string; action?: string; page?: number; limit?: number; }; export type DeploymentEntity = { id: string; serviceId: string; commitHash?: string; status: string; imageName?: string; imageTag?: string; buildLog?: string; runtimeLog?: string; error?: string; startedAt?: string; completedAt?: string; createdAt?: string; updatedAt?: string; }; export type CreateDeploymentInput = { commitHash?: string; branch?: string; trigger?: string; envVars?: Record; }; export type RollbackDeploymentResult = { deployment?: DeploymentEntity; message: string; }; export type ServiceLogEntity = { timestamp?: string; message: string; stream: string; }; export type ListServiceLogsInput = { tail?: string; follow?: boolean; }; export type GetDeploymentLogsInput = { type?: 'all' | 'build' | 'runtime'; }; type RawProject = components['schemas']['Project']; type RawService = components['schemas']['Service']; type RawUserProfile = components['schemas']['User']; type RawServiceVariable = components['schemas']['ServiceVariable']; type RawBuildStatus = components['schemas']['BuildStatus']; type RawBuildListResponse = components['schemas']['BuildListResponse']; type RawServiceTemplate = components['schemas']['ServiceTemplate']; type RawTemplateConfig = components['schemas']['TemplateConfig']; type RawTemplateVariable = components['schemas']['TemplateVariable']; type RawTemplateDetailResponse = components['schemas']['TemplateDetailResponse']; type RawDeployTemplateResponse = components['schemas']['DeployTemplateResponse']; type RawAuditLog = components['schemas']['AuditLog']; type RawAuditLogListResponse = components['schemas']['AuditLogListResponse']; type RawDeployment = components['schemas']['Deployment']; type RawDeploymentListResponse = components['schemas']['DeploymentListResponse']; type RawCreateDeploymentRequest = components['schemas']['CreateDeploymentRequest']; type RawServiceLog = components['schemas']['ServiceLogEntry']; type RawServiceLogsResponse = components['schemas']['ServiceLogsResponse']; type RawDeploymentLogsResponse = components['schemas']['DeploymentLogsResponse']; type RawRollbackDeploymentResponse = components['schemas']['RollbackDeploymentResponse']; type RawUpgradeStatus = components['schemas']['UpgradeStatus']; type RawHostMonitoring = components['schemas']['HostMonitoring']; type RawSystemLoad = NonNullable; type RawNodeAgent = components['schemas']['NodeAgent']; export type CreateProjectInput = components['schemas']['CreateProjectRequest']; export type CreateServiceInput = components['schemas']['CreateServiceRequest']; export class ApiError extends Error { readonly status: number; constructor(message: string, status: number) { super(message); this.name = 'ApiError'; this.status = status; } } type ProjectsResponse200 = paths['/projects']['get']['responses'][200]['content']['application/json']; type JsonLike = Record; const rawBase = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8082'; const normalizedBase = rawBase.replace(/\/$/, ''); const API_ROOT = normalizedBase.replace(/\/api\/v1$/, '').replace(/\/api$/, ''); const API_BASE = /\/api\/v1$/.test(normalizedBase) ? normalizedBase : `${normalizedBase}/api/v1`; export function getApiBaseUrl(): string { return API_BASE; } export function getAgentPublicBaseUrl(): string { return `${API_ROOT}/api/agents`; } function authHeaders(): HeadersInit { return { 'Content-Type': 'application/json' }; } async function requestJson(path: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE}${path}`, { ...init, credentials: 'include', headers: { ...authHeaders(), ...(init?.headers ?? {}), }, }); if (response.status === 204) { return undefined as T; } const payload = (await response.json().catch(() => null)) as JsonLike | null; if (!response.ok) { const message = (payload?.error as string | undefined) ?? (payload?.message as string | undefined) ?? `Request failed with status ${response.status}`; throw new ApiError(message, response.status); } return payload as T; } async function requestText(path: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE}${path}`, { ...init, credentials: 'include', headers: { ...authHeaders(), ...(init?.headers ?? {}), }, }); const text = await response.text(); if (!response.ok) { throw new ApiError(text || `Request failed with status ${response.status}`, response.status); } return text; } function normalizeProject(project: RawProject): ProjectEntity | null { if (!project.id || !project.name) { return null; } return { id: project.id, name: project.name, description: project.description ?? '', createdAt: project.created_at, updatedAt: project.updated_at, stats: { service_count: project.stats?.service_count ?? 0, deployment_count: project.stats?.deployment_count ?? 0, running_services: project.stats?.running_services ?? 0, last_deployment: project.stats?.last_deployment ?? null, }, }; } function normalizeUserProfile(profile: RawUserProfile): UserProfile | null { if (!profile.id || !profile.email || !profile.name) { return null; } return { id: profile.id, email: profile.email, name: profile.name, avatarUrl: profile.avatar_url ?? undefined, createdAt: profile.created_at ?? undefined, updatedAt: profile.updated_at ?? undefined, }; } function normalizeService(service: RawService): ServiceEntity | null { if (!service.id || !service.name || !service.project_id) { return null; } const status = (service.status ?? 'unknown') as ServiceStatus; return { id: service.id, projectId: service.project_id, name: service.name, type: service.type ?? 'web', status, createdAt: service.created_at, updatedAt: service.updated_at, image: service.image, command: service.command, environment: service.environment, gitRepo: service.git_repo, gitBranch: service.git_branch, buildPath: service.build_path, cpu: service.cpu, memory: service.memory, }; } function normalizeProjectArray(raw: unknown): ProjectEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeProject(entry as RawProject)) .filter((entry): entry is ProjectEntity => entry !== null); } function normalizeServiceArray(raw: unknown): ServiceEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeService(entry as RawService)) .filter((entry): entry is ServiceEntity => entry !== null); } function normalizeBuild(build: RawBuildStatus): BuildEntity | null { if (!build.id || !build.status) { return null; } return { id: build.id, projectId: build.project_id, serviceId: build.service_id, status: build.status as BuildStatus, progress: build.progress ?? 0, startedAt: build.started_at, completedAt: build.completed_at, imageName: build.image_name, imageTag: build.image_tag, size: build.size ?? 0, error: build.error, log: build.log, metadata: build.metadata ?? {}, }; } function normalizeBuildArray(raw: unknown): BuildEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeBuild(entry as RawBuildStatus)) .filter((entry): entry is BuildEntity => entry !== null); } function normalizeTemplate(template: RawServiceTemplate): TemplateEntity | null { if (!template.id || !template.name) { return null; } return { id: template.id, name: template.name, description: template.description ?? '', category: template.category ?? '', logo: template.logo ?? '', configRaw: template.config ?? '', variablesRaw: template.variables ?? '', isOfficial: Boolean(template.is_official), createdAt: template.created_at, updatedAt: template.updated_at, }; } function normalizeTemplateArray(raw: unknown): TemplateEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeTemplate(entry as RawServiceTemplate)) .filter((entry): entry is TemplateEntity => entry !== null); } function normalizeStringRecord(raw: unknown): Record { if (!raw || typeof raw !== 'object') { return {}; } const result: Record = {}; for (const [key, value] of Object.entries(raw as Record)) { if (typeof value === 'string') { result[key] = value; } } return result; } function normalizeTemplateConfig(config?: RawTemplateConfig): TemplateConfigEntity { return { type: config?.type ?? '', runtime: config?.runtime ?? '', buildCommand: config?.build_command ?? '', startCommand: config?.start_command ?? '', port: config?.port ?? 0, healthCheck: config?.health_check ?? '', environment: normalizeStringRecord(config?.environment), dockerfile: config?.dockerfile ?? undefined, nixpacksConfig: normalizeStringRecord(config?.nixpacks_config), }; } function normalizeTemplateVariable(variable: RawTemplateVariable): TemplateVariableEntity | null { if (!variable.key) { return null; } return { key: variable.key, label: variable.label ?? variable.key, defaultValue: variable.default ?? '', required: Boolean(variable.required), secret: Boolean(variable.secret), description: variable.description ?? '', }; } function normalizeTemplateVariableArray(raw: unknown): TemplateVariableEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeTemplateVariable(entry as RawTemplateVariable)) .filter((entry): entry is TemplateVariableEntity => entry !== null); } function normalizeAuditLog(log: RawAuditLog): AuditLogEntity | null { if (!log.id || !log.user_id || !log.resource || !log.action) { return null; } return { id: log.id, userId: log.user_id, userEmail: log.user_email ?? '', resource: log.resource, resourceId: log.resource_id ?? '', action: log.action, details: log.details ?? '', ipAddress: log.ip_address ?? '', userAgent: log.user_agent ?? '', createdAt: log.created_at, }; } function normalizeAuditLogArray(raw: unknown): AuditLogEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeAuditLog(entry as RawAuditLog)) .filter((entry): entry is AuditLogEntity => entry !== null); } function normalizeDeployment(deployment: RawDeployment): DeploymentEntity | null { if (!deployment.id || !deployment.service_id || !deployment.status) { return null; } return { id: deployment.id, serviceId: deployment.service_id, commitHash: deployment.commit_hash ?? undefined, status: deployment.status, imageName: deployment.image_name ?? undefined, imageTag: deployment.image_tag ?? undefined, buildLog: deployment.build_log ?? undefined, runtimeLog: deployment.runtime_log ?? undefined, error: deployment.error ?? undefined, startedAt: deployment.started_at ?? undefined, completedAt: deployment.completed_at ?? undefined, createdAt: deployment.created_at ?? undefined, updatedAt: deployment.updated_at ?? undefined, }; } function normalizeDeploymentArray(raw: unknown): DeploymentEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeDeployment(entry as RawDeployment)) .filter((entry): entry is DeploymentEntity => entry !== null); } function normalizeServiceLog(log: RawServiceLog): ServiceLogEntity | null { if (!log.message || !log.stream) { return null; } return { timestamp: log.timestamp ?? undefined, message: log.message, stream: log.stream, }; } function normalizeServiceLogArray(raw: unknown): ServiceLogEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeServiceLog(entry as RawServiceLog)) .filter((entry): entry is ServiceLogEntity => entry !== null); } export async function listProjects(): Promise { const payload = await requestJson(`/projects`); return normalizeProjectArray((payload as { projects?: RawProject[] }).projects); } export async function getProjectById(projectId: string): Promise { const payload = await requestJson(`/projects/${projectId}`); const rawProject = (payload as { project?: RawProject }).project ?? (payload as RawProject); const parsed = normalizeProject(rawProject); if (!parsed) { throw new ApiError('Project payload is invalid', 500); } return parsed; } export async function getCurrentUserProfile(): Promise { const payload = await requestJson(`/user/profile`); const rawProfile = (payload as { user?: RawUserProfile }).user ?? (payload as RawUserProfile); const profile = normalizeUserProfile(rawProfile); if (!profile) { throw new ApiError('User profile payload is invalid', 500); } return profile; } export async function updateCurrentUserProfile(input: UpdateUserProfileInput): Promise { const payload = await requestJson(`/user/profile`, { method: 'PUT', body: JSON.stringify({ name: input.name, avatar_url: input.avatarUrl, }), }); const rawProfile = (payload as { user?: RawUserProfile }).user ?? (payload as RawUserProfile); const profile = normalizeUserProfile(rawProfile); if (!profile) { throw new ApiError('Updated user profile payload is invalid', 500); } return profile; } export async function createUser(input: CreateUserInput): Promise { const payload = await requestJson(`/users`, { method: 'POST', body: JSON.stringify(input), }); const rawProfile = (payload as { user?: RawUserProfile }).user ?? (payload as RawUserProfile); const profile = normalizeUserProfile(rawProfile); if (!profile) { throw new ApiError('Create user response is invalid', 500); } return profile; } function normalizeUpgradeStatus(payload: RawUpgradeStatus): UpgradeStatus { return { imageRef: payload.image_ref ?? '', registry: payload.registry ?? '', dockerAvailable: Boolean(payload.docker_available), installed: Boolean(payload.installed), digest: payload.digest, size: payload.size, authConfigured: Boolean(payload.auth_configured), message: payload.message ?? '', }; } function normalizeSystemLoad(payload?: RawSystemLoad): SystemLoad { return { load1m: payload?.load_1m ?? 0, load5m: payload?.load_5m ?? 0, load15m: payload?.load_15m ?? 0, }; } function normalizeHostMonitoring(payload: RawHostMonitoring): HostMonitoring { return { hostname: payload.hostname ?? '', os: payload.os ?? '', architecture: payload.architecture ?? '', cpu: { cores: payload.cpu?.cores ?? 0, }, memory: { total: payload.memory?.total ?? 0, used: payload.memory?.used ?? 0, available: payload.memory?.available ?? 0, usagePercent: payload.memory?.usage_percent ?? 0, }, storage: { path: payload.storage?.path ?? '/', total: payload.storage?.total ?? 0, used: payload.storage?.used ?? 0, available: payload.storage?.available ?? 0, usagePercent: payload.storage?.usage_percent ?? 0, }, load: normalizeSystemLoad(payload.load), uptimeSeconds: payload.uptime_seconds ?? 0, dockerAvailable: Boolean(payload.docker_available), docker: payload.docker, collectedAt: payload.collected_at ?? '', }; } function normalizeAgent(agent: RawNodeAgent): NodeAgentEntity | null { if (!agent.id || !agent.name) { return null; } return { id: agent.id, name: agent.name, hostname: agent.hostname ?? '', ipAddress: agent.ip_address ?? '', port: agent.port ?? 0, status: agent.status ?? 'unknown', version: agent.version ?? '', capabilities: { containerRuntimes: agent.capabilities?.container_runtimes ?? [], supportedArchitectures: agent.capabilities?.supported_architectures ?? [], maxContainers: agent.capabilities?.max_containers ?? 0, storageDriver: agent.capabilities?.storage_driver ?? '', networkPlugins: agent.capabilities?.network_plugins ?? [], features: agent.capabilities?.features ?? [], }, resources: { cpu: { cores: agent.resources?.cpu?.cores ?? 0, allocation: agent.resources?.cpu?.allocation ?? 0, usage: agent.resources?.cpu?.usage ?? 0, }, memory: { total: agent.resources?.memory?.total ?? 0, allocated: agent.resources?.memory?.allocated ?? 0, used: agent.resources?.memory?.used ?? 0, available: agent.resources?.memory?.available ?? 0, }, storage: { total: agent.resources?.storage?.total ?? 0, allocated: agent.resources?.storage?.allocated ?? 0, used: agent.resources?.storage?.used ?? 0, available: agent.resources?.storage?.available ?? 0, }, }, lastHeartbeat: agent.last_heartbeat, createdAt: agent.created_at, updatedAt: agent.updated_at, }; } function normalizeAgentArray(raw: unknown): NodeAgentEntity[] { if (!Array.isArray(raw)) { return []; } return raw .map((entry) => normalizeAgent(entry as RawNodeAgent)) .filter((entry): entry is NodeAgentEntity => entry !== null); } export async function getUpgradeStatus(): Promise { const payload = await requestJson(`/system/upgrade/status`); return normalizeUpgradeStatus(payload); } export async function pullUpgradeImage(): Promise { const payload = await requestJson(`/system/upgrade/pull`, { method: 'POST', body: JSON.stringify({}), }); return normalizeUpgradeStatus(payload); } export async function getHostMonitoring(): Promise { const payload = await requestJson(`/system/host`); return normalizeHostMonitoring(payload); } export async function listAgents(): Promise { const payload = await requestJson<{ agents?: RawNodeAgent[] }>(`/agents`); return normalizeAgentArray(payload.agents); } export async function createProject(input: CreateProjectInput): Promise { const payload = await requestJson(`/projects`, { method: 'POST', body: JSON.stringify(input), }); const rawProject = (payload as { project?: RawProject }).project ?? (payload as RawProject); const parsed = normalizeProject(rawProject); if (!parsed) { throw new ApiError('Create project response is invalid', 500); } return parsed; } export async function listServicesByProject(projectId: string): Promise { const payload = await requestJson(`/projects/${projectId}/services`); const rows = Array.isArray(payload) ? payload : payload.services ?? []; return normalizeServiceArray(rows); } export async function getServiceById(serviceId: string): Promise { const payload = await requestJson(`/services/${serviceId}`); const row = (payload as { service?: RawService }).service ?? (payload as RawService); const parsed = normalizeService(row); if (!parsed) { throw new ApiError('Service payload is invalid', 500); } return parsed; } export async function createService(projectId: string, input: CreateServiceInput): Promise { const payload = await requestJson(`/projects/${projectId}/services`, { method: 'POST', body: JSON.stringify({ ...input, project_id: projectId }), }); const row = (payload as { service?: RawService }).service ?? (payload as RawService); const parsed = normalizeService(row); if (!parsed) { throw new ApiError('Create service response is invalid', 500); } return parsed; } export async function deleteService(serviceId: string): Promise { await requestJson<{ message?: string }>(`/services/${serviceId}`, { method: 'DELETE', }); } export async function listServiceVariables(serviceId: string): Promise { const payload = await requestJson<{ variables?: RawServiceVariable[] }>(`/services/${serviceId}/variables`); const rows = payload.variables ?? []; const result: ServiceVariable[] = []; for (const row of rows) { if (!row.id || !row.service_id || !row.key) { continue; } result.push({ id: row.id, serviceId: row.service_id, key: row.key, value: row.value ?? '', isSecret: Boolean(row.is_secret), createdAt: row.created_at, updatedAt: row.updated_at, }); } return result; } export async function listAuditLogs(input: ListAuditLogsInput = {}): Promise { const searchParams = new URLSearchParams(); if (input.resource) { searchParams.set('resource', input.resource); } if (input.action) { searchParams.set('action', input.action); } if (input.page && input.page > 0) { searchParams.set('page', String(input.page)); } if (input.limit && input.limit > 0) { searchParams.set('limit', String(input.limit)); } const query = searchParams.toString(); const payload = await requestJson(query ? `/audit-logs?${query}` : '/audit-logs'); return normalizeAuditLogArray(payload.audit_logs); } export async function listDeployments(serviceId: string): Promise { const payload = await requestJson(`/services/${serviceId}/deployments`); return normalizeDeploymentArray(payload.deployments); } export async function createDeployment( serviceId: string, input: CreateDeploymentInput = {}, ): Promise { const requestBody: RawCreateDeploymentRequest = { commit_hash: input.commitHash, branch: input.branch, trigger: input.trigger, env_vars: input.envVars, }; const payload = await requestJson(`/services/${serviceId}/deployments`, { method: 'POST', body: JSON.stringify(requestBody), }); const deployment = normalizeDeployment(payload); if (!deployment) { throw new ApiError('Create deployment response is invalid', 500); } return deployment; } export async function listServiceLogs( serviceId: string, input: ListServiceLogsInput = {}, ): Promise { const searchParams = new URLSearchParams(); if (input.tail) { searchParams.set('tail', input.tail); } if (typeof input.follow === 'boolean') { searchParams.set('follow', String(input.follow)); } const query = searchParams.toString(); const payload = await requestJson( query ? `/services/${serviceId}/logs?${query}` : `/services/${serviceId}/logs`, ); return normalizeServiceLogArray(payload.logs); } export async function getDeploymentLogs( deploymentId: string, input: GetDeploymentLogsInput = {}, ): Promise<{ logs: ServiceLogEntity[]; buildLog: string; runtimeLog: string; }> { const searchParams = new URLSearchParams(); if (input.type) { searchParams.set('type', input.type); } const query = searchParams.toString(); const payload = await requestJson( query ? `/deployments/${deploymentId}/logs?${query}` : `/deployments/${deploymentId}/logs`, ); return { logs: normalizeServiceLogArray(payload.logs), buildLog: payload.build_log ?? '', runtimeLog: payload.runtime_log ?? '', }; } export async function rollbackDeployment(deploymentId: string): Promise { const payload = await requestJson(`/deployments/${deploymentId}/rollback`, { method: 'POST', }); return { deployment: payload.deployment ? normalizeDeployment(payload.deployment) ?? undefined : undefined, message: payload.message ?? 'Rollback initiated', }; } export async function listBuilds(input: ListBuildsInput = {}): Promise { const searchParams = new URLSearchParams(); if (input.projectId) { searchParams.set('project_id', input.projectId); } if (input.serviceId) { searchParams.set('service_id', input.serviceId); } if (input.status) { searchParams.set('status', input.status); } if (input.page && input.page > 0) { searchParams.set('page', String(input.page)); } if (input.limit && input.limit > 0) { searchParams.set('limit', String(input.limit)); } const query = searchParams.toString(); const payload = await requestJson(query ? `/builds?${query}` : '/builds'); return { builds: normalizeBuildArray(payload.builds), total: payload.total ?? 0, page: payload.page ?? input.page ?? 1, limit: payload.limit ?? input.limit ?? 20, }; } export async function cancelBuild(buildId: string): Promise { const payload = await requestJson<{ message?: string }>(`/builds/${buildId}/cancel`, { method: 'POST', }); return payload.message ?? 'Build cancelled'; } export async function getBuildLogs(buildId: string, follow = false): Promise { const query = follow ? '?follow=true' : ''; return requestText(`/builds/${buildId}/logs${query}`); } export async function listTemplates(input: ListTemplatesInput = {}): Promise { const searchParams = new URLSearchParams(); if (input.category) { searchParams.set('category', input.category); } const query = searchParams.toString(); const payload = await requestJson<{ templates?: RawServiceTemplate[] }>( query ? `/templates?${query}` : '/templates', ); return normalizeTemplateArray(payload.templates); } export async function getTemplateById(templateId: string): Promise { const payload = await requestJson(`/templates/${templateId}`); const template = payload.template ? normalizeTemplate(payload.template) : null; if (!template) { throw new ApiError('Template payload is invalid', 500); } return { template, config: normalizeTemplateConfig(payload.config), variables: normalizeTemplateVariableArray(payload.variables), }; } export async function deployTemplate( templateId: string, input: DeployTemplateInput, ): Promise { const payload = await requestJson(`/templates/${templateId}/deploy`, { method: 'POST', body: JSON.stringify({ project_id: input.projectId, name: input.name, variables: input.variables ?? {}, }), }); if (!payload.service_id) { throw new ApiError('Template deployment response is invalid', 500); } return { serviceId: payload.service_id, message: payload.message ?? 'Service created from template', }; } export function serviceStatusClass(status: ServiceStatus): string { switch (status) { case 'running': return 'status-running'; case 'building': return 'status-building'; case 'failed': return 'status-failed'; case 'stopped': return 'status-stopped'; default: return 'status-stopped'; } } export type ServiceInstanceMetrics = components['schemas']['ServiceInstanceMetrics']; export type ServiceMetrics = components['schemas']['ServiceMetrics']; export async function getServiceMetrics(serviceId: string): Promise { const payload = await requestJson<{ metrics?: ServiceMetrics }>(`/services/${serviceId}/metrics`); if (!payload.metrics) { throw new ApiError('Service metrics payload is invalid', 500); } return payload.metrics; } export type GitProviderEntity = components['schemas']['GitProvider']; export type GitRepositoryEntity = components['schemas']['GitRepository']; export type GitBranchEntity = components['schemas']['GitBranch']; export type CreateGitProviderInput = components['schemas']['CreateGitProviderRequest']; export async function listGitProviders(): Promise { const payload = await requestJson<{ providers?: GitProviderEntity[] }>('/git/providers'); return payload.providers ?? []; } export async function createGitProvider(input: CreateGitProviderInput): Promise { const payload = await requestJson('/git/providers', { method: 'POST', body: JSON.stringify(input), }); if (!payload.id) { throw new ApiError('Provider response is invalid', 500); } return payload; } export async function deleteGitProvider(providerId: string): Promise { await requestJson(`/git/providers/${providerId}`, { method: 'DELETE' }); } export async function listGitRepositories( providerId: string, search?: string, ): Promise { const params = new URLSearchParams({ limit: '100' }); if (search?.trim()) { params.set('search', search.trim()); } const payload = await requestJson<{ repositories?: GitRepositoryEntity[] }>( `/git/providers/${providerId}/repositories?${params.toString()}`, ); return payload.repositories ?? []; } export async function listGitBranches( providerId: string, owner: string, repo: string, ): Promise { const payload = await requestJson<{ branches?: GitBranchEntity[] }>( `/git/providers/${providerId}/repositories/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`, ); return payload.branches ?? []; } export type CreateDatabaseInput = components['schemas']['CreateDatabaseRequest']; export type DatabaseEntity = components['schemas']['Database']; export async function createManagedDatabase( input: CreateDatabaseInput, ): Promise<{ id: string; status: string }> { const payload = await requestJson<{ id?: string; status?: string }>('/databases', { method: 'POST', body: JSON.stringify(input), }); if (!payload.id) { throw new ApiError('Database response is invalid', 500); } return { id: payload.id, status: payload.status ?? 'building' }; } export type ConnectGitRepositoryInput = components['schemas']['ConnectGitRepoRequest']; export type CreateGitWebhookInput = components['schemas']['CreateWebhookRequest']; export type GitWebhookEntity = components['schemas']['GitWebhook']; export async function connectGitRepository( input: ConnectGitRepositoryInput, ): Promise { const payload = await requestJson<{ repository?: GitRepositoryEntity }>( '/git/repositories/connect', { method: 'POST', body: JSON.stringify(input) }, ); if (!payload.repository?.id) { throw new ApiError('Connect repository response is invalid', 500); } return payload.repository; } export async function listConnectedGitRepositories(): Promise { const payload = await requestJson<{ repositories?: GitRepositoryEntity[] }>('/git/repositories'); return payload.repositories ?? []; } export async function createGitWebhook(input: CreateGitWebhookInput): Promise { const payload = await requestJson<{ webhook?: GitWebhookEntity }>('/git/webhooks', { method: 'POST', body: JSON.stringify(input), }); if (!payload.webhook?.id) { throw new ApiError('Webhook response is invalid', 500); } return payload.webhook; }