import type { components, paths } from '@/generated/api-types'; export type ServiceStatus = 'running' | 'degraded' | 'stopped' | 'building' | 'failed' | 'unknown'; export type ProjectStats = components['schemas']['ProjectStats']; export type ProjectEntity = { id: string; name: string; description: string; isApproved: boolean; createdAt?: string; updatedAt?: string; stats: ProjectStats; }; export type UserProfile = { id: string; email: string; name: string; avatarUrl?: string; isAdmin: boolean; createdAt?: string; updatedAt?: string; }; export type PlatformSettings = { signupEnabled: boolean; cloudflareTunnel: { tokenSet: boolean; source: 'app' | 'env' | 'none'; container: 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; replicas?: number; port?: number; domain?: string; healthcheckPath?: string; restartPolicy?: string; volumes?: ServiceVolume[]; domains?: ServiceDomain[]; maintenanceMode?: boolean; basicAuth?: string[]; builder?: 'auto' | 'railpack' | 'nixpacks' | 'dockerfile' | 'static'; cpuReserve?: string; memoryReserve?: string; staticBuildCmd?: string; staticDir?: string; publicUrl?: string; }; export type ServiceDomain = { id: string; service_id: string; domain: string; is_default: boolean; cert_type?: string; cert_status?: string; last_checked_at?: string; created_at?: string; }; export type DomainCheckResult = { domain: string; status: 'ok' | 'wrong-target' | 'pending'; resolved?: string[]; }; export type ServiceVolume = { type?: 'volume' | 'bind'; source: string; target: string; read_only?: boolean; }; export type RuntimeContainer = { id: string; name: string; state: string; replica: number; }; export type ServiceRuntime = { desired: number; status: 'running' | 'degraded' | 'stopped'; urls: string[]; ports: number[]; health?: 'healthy' | 'unhealthy' | 'unreachable' | ''; containers: RuntimeContainer[]; }; 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; ownerId: string | null; isPublic: 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; os?: string; }; scope: 'host' | 'docker-desktop-vm'; 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; actor?: string; userId?: string; since?: 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 type UpdateServiceInput = components['schemas']['UpdateServiceRequest']; 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 configuredBase = ((import.meta.env.VITE_API_URL as string | undefined) ?? '').trim(); const rawBase = configuredBase || window.location.origin; 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); } // SPA fallback servers may answer /api/* with index.html — an ok response // whose body is not JSON. Surface an empty object so envelope unwraps // (`payload.x ?? []`) degrade instead of dereferencing null. 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 ?? '', isApproved: project.is_approved ?? false, 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, isAdmin: profile.is_admin ?? false, createdAt: profile.created_at ?? undefined, updatedAt: profile.updated_at ?? undefined, }; } function normalizePlatformSettings(raw: components['schemas']['PlatformSettings']): PlatformSettings { return { signupEnabled: raw.signup_enabled ?? false, cloudflareTunnel: { tokenSet: raw.cloudflare_tunnel?.token_set ?? false, source: raw.cloudflare_tunnel?.source ?? 'none', container: raw.cloudflare_tunnel?.container ?? 'missing', }, }; } export async function getPlatformSettings(): Promise { const raw = await requestJson('/settings'); return normalizePlatformSettings(raw); } export async function updatePlatformSettings(input: { signupEnabled?: boolean; cloudflareTunnelToken?: string; }): Promise { const body: Record = {}; if (input.signupEnabled !== undefined) { body.signup_enabled = input.signupEnabled; } if (input.cloudflareTunnelToken !== undefined) { body.cloudflare_tunnel_token = input.cloudflareTunnelToken; } const raw = await requestJson('/settings', { method: 'PUT', body: JSON.stringify(body), }); return normalizePlatformSettings(raw); } 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, replicas: service.replicas, port: service.port, domain: service.domain, healthcheckPath: service.healthcheck_path, restartPolicy: service.restart_policy, volumes: service.volumes as ServiceVolume[] | undefined, domains: service.domains as ServiceDomain[] | undefined, maintenanceMode: service.maintenance_mode, basicAuth: service.basic_auth as string[] | undefined, builder: service.builder as ServiceEntity['builder'], cpuReserve: service.cpu_reserve, memoryReserve: service.memory_reserve, staticBuildCmd: service.static_build_cmd, staticDir: service.static_dir, publicUrl: service.public_url, }; } 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), ownerId: template.owner_id ?? null, isPublic: Boolean(template.is_public), 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(params?: { limit?: number; search?: string }): Promise { const search = new URLSearchParams(); if (params?.limit) search.set('limit', String(params.limit)); if (params?.search) search.set('search', params.search); const qs = search.toString(); const payload = await requestJson(`/projects${qs ? `?${qs}` : ''}`); 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, scope: payload.scope === 'docker-desktop-vm' ? 'docker-desktop-vm' : 'host', 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 type AgentAuthToken = components['schemas']['AgentAuthToken']; export type AgentAuthTokenCreated = components['schemas']['AgentAuthTokenCreated']; export async function createAgentToken(label?: string): Promise { return requestJson(`/agent-tokens`, { method: 'POST', body: JSON.stringify({ label: label ?? '' }), }); } export async function listAgentTokens(): Promise { const payload = await requestJson<{ tokens?: AgentAuthToken[] }>(`/agent-tokens`); return payload.tokens ?? []; } export async function revokeAgentToken(id: string): Promise { await requestJson(`/agent-tokens/${id}`, { method: 'DELETE' }); } export type UserToken = components['schemas']['UserToken']; export type UserTokenCreated = components['schemas']['UserTokenCreated']; export type UserTokenScope = 'read' | 'write' | 'admin'; export async function createUserToken(input: { name: string; scope?: UserTokenScope; expiresInDays?: number; }): Promise { return requestJson(`/user/tokens`, { method: 'POST', body: JSON.stringify({ name: input.name, scope: input.scope ?? 'write', expires_in_days: input.expiresInDays ?? 0, }), }); } export async function listUserTokens(): Promise { const payload = await requestJson<{ tokens?: UserToken[] }>(`/user/tokens`); return payload.tokens ?? []; } export async function revokeUserToken(id: string): Promise { await requestJson(`/user/tokens/${id}`, { method: 'DELETE' }); } 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 updateProject( projectId: string, input: { name?: string; description?: string }, ): Promise { const payload = await requestJson( `/projects/${encodeURIComponent(projectId)}`, { method: 'PUT', body: JSON.stringify(input) }, ); const rawProject = (payload as { project?: RawProject }).project ?? (payload as RawProject); const parsed = normalizeProject(rawProject); if (!parsed) { throw new ApiError('Update project response is invalid', 500); } return parsed; } export async function deleteProject(projectId: string): Promise { await requestJson(`/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE' }); } 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 listServiceDomains(serviceId: string): Promise { const payload = await requestJson<{ domains?: ServiceDomain[] }>(`/services/${serviceId}/domains`); return payload.domains ?? []; } export async function addServiceDomain(serviceId: string, domain: string, isDefault = false): Promise { const payload = await requestJson<{ domain: ServiceDomain }>(`/services/${serviceId}/domains`, { method: 'POST', body: JSON.stringify({ domain, is_default: isDefault }), }); return payload.domain; } export async function removeServiceDomain(serviceId: string, domainId: string): Promise { await requestJson(`/services/${serviceId}/domains/${domainId}`, { method: 'DELETE' }); } export async function setDefaultServiceDomain(serviceId: string, domainId: string): Promise { const payload = await requestJson<{ domains?: ServiceDomain[] }>( `/services/${serviceId}/domains/${domainId}/default`, { method: 'POST' }, ); return payload.domains ?? []; } export async function checkServiceDomains(serviceId: string): Promise { const payload = await requestJson<{ domains?: DomainCheckResult[] }>(`/services/${serviceId}/domains/check`); return payload.domains ?? []; } export async function updateService(serviceId: string, input: UpdateServiceInput): Promise { const payload = await requestJson(`/services/${serviceId}`, { method: 'PUT', body: JSON.stringify(input), }); const row = (payload as { service?: RawService }).service ?? (payload as RawService); const parsed = normalizeService(row); if (!parsed) { throw new ApiError('Update service response is invalid', 500); } return parsed; } export async function deleteService(serviceId: string): Promise { await requestJson<{ message?: string }>(`/services/${serviceId}`, { method: 'DELETE', }); } export type CloneServiceInput = { name?: string; project_id?: string; environment?: string; }; export async function cloneService(serviceId: string, input: CloneServiceInput): Promise<{ service_id: string; name: string }> { return requestJson<{ service_id: string; name: string }>(`/services/${serviceId}/clone`, { method: 'POST', body: JSON.stringify(input), }); } export async function moveService(serviceId: string, projectId: string): Promise { await requestJson(`/services/${serviceId}/move`, { method: 'POST', body: JSON.stringify({ project_id: projectId }), }); } export type Registry = { id: string; name: string; host: string; username?: string; has_password: boolean; created_at?: string; updated_at?: string; }; export type RegistryInput = { name: string; host: string; username?: string; password?: string; }; export async function listRegistries(): Promise { const payload = await requestJson<{ registries?: Registry[] }>('/registries'); return payload.registries ?? []; } export async function createRegistry(input: RegistryInput): Promise { const payload = await requestJson<{ registry: Registry }>('/registries', { method: 'POST', body: JSON.stringify(input), }); return payload.registry; } export async function updateRegistry(id: string, input: RegistryInput): Promise { const payload = await requestJson<{ registry: Registry }>(`/registries/${id}`, { method: 'PUT', body: JSON.stringify(input), }); return payload.registry; } export async function deleteRegistry(id: string): Promise { await requestJson(`/registries/${id}`, { method: 'DELETE' }); } function normalizeRuntime(runtime?: ServiceRuntime): ServiceRuntime { const state = runtime ?? { desired: 0, status: 'stopped' as const, urls: [], ports: [], containers: [] }; // The API emits null (not []) for empty collections — coerce at the boundary. return { ...state, urls: state.urls ?? [], ports: state.ports ?? [], containers: state.containers ?? [] }; } export async function getServiceRuntime(serviceId: string): Promise { const payload = await requestJson<{ runtime?: ServiceRuntime }>(`/services/${serviceId}/runtime`); return normalizeRuntime(payload.runtime); } async function serviceAction(serviceId: string, action: 'start' | 'stop' | 'restart'): Promise { await requestJson<{ message?: string }>(`/services/${serviceId}/${action}`, { method: 'POST' }); } export const startService = (serviceId: string) => serviceAction(serviceId, 'start'); export const stopService = (serviceId: string) => serviceAction(serviceId, 'stop'); export const restartService = (serviceId: string) => serviceAction(serviceId, 'restart'); export async function redeployService(serviceId: string): Promise { const payload = await requestJson<{ runtime?: ServiceRuntime }>(`/services/${serviceId}/redeploy`, { method: 'POST', }); return normalizeRuntime(payload.runtime); } function normalizeServiceVariables(rows: RawServiceVariable[] | undefined): ServiceVariable[] { 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 listServiceVariables(serviceId: string): Promise { const payload = await requestJson<{ variables?: RawServiceVariable[] }>(`/services/${serviceId}/variables`); return normalizeServiceVariables(payload.variables); } export type UpdateServiceVariableInput = components['schemas']['VariableInput']; export async function updateServiceVariables( serviceId: string, variables: UpdateServiceVariableInput[], ): Promise { const payload = await requestJson<{ variables?: RawServiceVariable[] }>( `/services/${serviceId}/variables`, { method: 'PUT', body: JSON.stringify({ variables }) }, ); return normalizeServiceVariables(payload.variables); } export async function listProjectVariables(projectId: string): Promise { const payload = await requestJson<{ variables?: RawServiceVariable[] }>(`/projects/${projectId}/variables`); return normalizeServiceVariables(payload.variables); } export async function updateProjectVariables( projectId: string, variables: UpdateServiceVariableInput[], ): Promise { const payload = await requestJson<{ variables?: RawServiceVariable[] }>( `/projects/${projectId}/variables`, { method: 'PUT', body: JSON.stringify({ variables }) }, ); return normalizeServiceVariables(payload.variables); } 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.actor) { searchParams.set('actor', input.actor); } if (input.userId) { searchParams.set('user_id', input.userId); } if (input.since) { searchParams.set('since', input.since); } 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 type RecentDeploymentEntity = { id: string; serviceId: string; serviceName: string; projectName: string; status: string; imageName?: string; startedAt?: string; completedAt?: string; createdAt?: string; }; export async function listRecentDeployments(limit = 10): Promise { const payload = await requestJson<{ deployments?: Array> }>( `/deployments?limit=${limit}`, ); return (payload.deployments ?? []) .map((d) => ({ id: String(d.id ?? ''), serviceId: String(d.service_id ?? ''), serviceName: String(d.service_name ?? ''), projectName: String(d.project_name ?? ''), status: String(d.status ?? ''), imageName: d.image_name ? String(d.image_name) : undefined, startedAt: d.started_at ? String(d.started_at) : undefined, completedAt: d.completed_at ? String(d.completed_at) : undefined, createdAt: d.created_at ? String(d.created_at) : undefined, })) .filter((d) => d.id !== ''); } 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 cancelDeployment(deploymentId: string): Promise<{ status: string }> { return requestJson<{ status: string }>(`/deployments/${deploymentId}/cancel`, { method: 'POST', }); } 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 type TemplateWriteInput = { name: string; description?: string; category?: string; logo?: string; config: Record; variables?: TemplateVariableEntity[]; isPublic?: boolean; }; function templateWriteBody(input: TemplateWriteInput): string { return JSON.stringify({ name: input.name, description: input.description ?? '', category: input.category ?? '', logo: input.logo ?? '', config: input.config, variables: input.variables ?? [], is_public: input.isPublic ?? false, }); } export async function createTemplate(input: TemplateWriteInput): Promise { const payload = await requestJson<{ template?: RawServiceTemplate }>('/templates', { method: 'POST', body: templateWriteBody(input), }); const template = payload.template ? normalizeTemplate(payload.template) : null; if (!template) { throw new ApiError('Template response is invalid', 500); } return template; } export async function updateTemplate(templateId: string, input: TemplateWriteInput): Promise { const payload = await requestJson<{ template?: RawServiceTemplate }>(`/templates/${templateId}`, { method: 'PUT', body: templateWriteBody(input), }); const template = payload.template ? normalizeTemplate(payload.template) : null; if (!template) { throw new ApiError('Template response is invalid', 500); } return template; } export async function deleteTemplate(templateId: string): Promise { await requestJson(`/templates/${templateId}`, { method: 'DELETE' }); } 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 'degraded': return 'status-degraded'; 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 type DatabaseBackupEntity = components['schemas']['DatabaseBackup']; export async function listDatabases(): Promise { const payload = await requestJson<{ databases?: DatabaseEntity[] }>('/databases'); return payload.databases ?? []; } export async function getDatabase(id: string): Promise { return requestJson(`/databases/${encodeURIComponent(id)}`); } export async function updateDatabase(id: string, input: components['schemas']['UpdateDatabaseRequest']): Promise { await requestJson(`/databases/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify(input), }); } export async function databaseAction(id: string, action: 'start' | 'stop' | 'restart'): Promise { await requestJson(`/databases/${encodeURIComponent(id)}/action`, { method: 'POST', body: JSON.stringify({ action }), }); } export async function updateDatabaseBackupSchedule(id: string, schedule: string): Promise { await requestJson(`/databases/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ backup_schedule: schedule }), }); } export async function createDatabaseBackup(id: string): Promise { await requestJson(`/databases/${encodeURIComponent(id)}/backup`, { method: 'POST', body: JSON.stringify({ database_id: id }), }); } export async function restoreDatabaseBackup(id: string, backupId: string): Promise { await requestJson(`/databases/${encodeURIComponent(id)}/restore`, { method: 'POST', body: JSON.stringify({ database_id: id, backup_id: backupId }), }); } export async function importDatabaseBackup(id: string, file: File): Promise<{ backup_id: string; size: string }> { const form = new FormData(); form.append('file', file); const resp = await fetch(`${getApiBaseUrl()}/databases/${encodeURIComponent(id)}/backups/import`, { method: 'POST', headers: authHeaders(), body: form, }); const payload = await resp.json().catch(() => ({})); if (!resp.ok) { throw new ApiError(payload.error || 'Import failed', resp.status); } return payload; } 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 ExternalDatabaseInput = { name?: string; type: string; host: string; port?: number; database?: string; username?: string; password?: string; ssl?: boolean; }; export async function registerExternalDatabase(input: ExternalDatabaseInput): Promise<{ id: string }> { const payload = await requestJson<{ id?: string }>('/databases/register-external', { method: 'POST', body: JSON.stringify(input), }); if (!payload.id) { throw new ApiError('Database response is invalid', 500); } return { id: payload.id }; } export async function testDatabaseConnection(input: ExternalDatabaseInput): Promise<{ ok: boolean; latency_ms?: number; error?: string }> { return requestJson('/databases/test-connection', { method: 'POST', body: JSON.stringify(input), }); } export async function testDatabaseConnectionByID(id: string): Promise<{ ok: boolean; latency_ms?: number; error?: string }> { return requestJson(`/databases/${encodeURIComponent(id)}/test-connection`, { method: 'POST' }); } export type BackupTarget = components['schemas']['BackupTarget']; export type BackupTargetInput = components['schemas']['BackupTargetRequest']; export async function listBackupTargets(): Promise { const payload = await requestJson<{ backup_targets?: BackupTarget[] }>('/backup-targets'); return payload.backup_targets ?? []; } export async function createBackupTarget(input: BackupTargetInput): Promise { const payload = await requestJson<{ backup_target?: BackupTarget }>('/backup-targets', { method: 'POST', body: JSON.stringify(input), }); if (!payload.backup_target) { throw new ApiError('Backup target response is invalid', 500); } return payload.backup_target; } export async function updateBackupTarget(id: string, input: BackupTargetInput): Promise { await requestJson(`/backup-targets/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify(input), }); } export async function deleteBackupTarget(id: string): Promise { await requestJson(`/backup-targets/${encodeURIComponent(id)}`, { method: 'DELETE' }); } export async function testBackupTarget(id: string): Promise<{ ok: boolean; latency_ms?: number; error?: string }> { return requestJson(`/backup-targets/${encodeURIComponent(id)}/test`, { method: 'POST' }); } export type ConnectGitRepositoryInput = components['schemas']['ConnectGitRepoRequest']; export type CreateGitWebhookInput = components['schemas']['CreateWebhookRequest']; export type GitWebhookEntity = components['schemas']['GitWebhook']; export async function getGitHubAppInstallUrl(): Promise { const payload = await requestJson<{ install_url?: string }>('/git/github-app/install-url'); if (!payload.install_url) { throw new ApiError('GitHub App install URL unavailable', 500); } return payload.install_url; } export async function connectGitHubApp(installationId: number, displayName?: string): Promise { const payload = await requestJson<{ provider?: GitProviderEntity }>('/git/github-app/connect', { method: 'POST', body: JSON.stringify({ installation_id: installationId, display_name: displayName }), }); if (!payload.provider?.id) { throw new ApiError('GitHub App connect response is invalid', 500); } return payload.provider; } 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; } // --- Cron jobs --- export type CronJobEntity = components['schemas']['CronJob']; export type CronExecutionEntity = components['schemas']['CronExecution']; export type CreateCronJobInput = components['schemas']['CreateCronJobRequest']; export type UpdateCronJobInput = components['schemas']['UpdateCronJobRequest']; export async function listCronJobs(serviceId: string): Promise { const payload = await requestJson<{ cron_jobs?: CronJobEntity[] }>( `/cron-jobs?service_id=${encodeURIComponent(serviceId)}`, ); return payload.cron_jobs ?? []; } export async function createCronJob(input: CreateCronJobInput): Promise { const payload = await requestJson<{ cron_job?: CronJobEntity }>('/cron-jobs', { method: 'POST', body: JSON.stringify(input), }); if (!payload.cron_job?.id) { throw new ApiError('Create cron job response is invalid', 500); } return payload.cron_job; } export async function updateCronJob(id: string, input: UpdateCronJobInput): Promise { await requestJson(`/cron-jobs/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify(input), }); } export async function deleteCronJob(id: string): Promise { await requestJson(`/cron-jobs/${encodeURIComponent(id)}`, { method: 'DELETE' }); } export async function listCronExecutions(id: string): Promise { const payload = await requestJson<{ executions?: CronExecutionEntity[] }>( `/cron-jobs/${encodeURIComponent(id)}/executions`, ); return payload.executions ?? []; } export async function triggerCronJob(id: string): Promise { await requestJson(`/cron-jobs/${encodeURIComponent(id)}/trigger`, { method: 'POST' }); } export type NotificationEntity = components['schemas']['Notification']; export type NotificationList = { notifications: NotificationEntity[]; unread: number; }; export async function listNotifications(limit = 20): Promise { const payload = await requestJson<{ notifications?: NotificationEntity[]; unread?: number }>( `/notifications?limit=${limit}`, ); return { notifications: payload.notifications ?? [], unread: payload.unread ?? 0 }; } export async function markNotificationRead(id: string): Promise { await requestJson(`/notifications/${encodeURIComponent(id)}/read`, { method: 'POST' }); } export async function markAllNotificationsRead(): Promise { await requestJson('/notifications/read-all', { method: 'POST' }); } export type PreviewEnvironmentEntity = components['schemas']['PreviewEnvironment']; export async function listPreviewEnvironments(projectId: string): Promise { const payload = await requestJson<{ preview_environments?: PreviewEnvironmentEntity[] }>( `/projects/${encodeURIComponent(projectId)}/preview-environments`, ); return payload.preview_environments ?? []; } export async function createPreviewEnvironment( projectId: string, input: components['schemas']['CreatePreviewEnvironmentRequest'], ): Promise { return requestJson( `/projects/${encodeURIComponent(projectId)}/preview-environments`, { method: 'POST', body: JSON.stringify(input) }, ); } export async function deletePreviewEnvironment(id: string): Promise { await requestJson(`/preview-environments/${encodeURIComponent(id)}`, { method: 'DELETE' }); } export async function promotePreviewEnvironment( id: string, input: components['schemas']['PromotePreviewEnvironmentRequest'], ): Promise { await requestJson(`/preview-environments/${encodeURIComponent(id)}/promote`, { method: 'POST', body: JSON.stringify(input), }); } export type ScalingPolicy = components['schemas']['ScalingPolicy']; export type ServiceScalingState = { ServiceID?: string; CurrentReplicas?: number; DesiredReplicas?: number; LastScaleAction?: string; LastScaleDirection?: string; }; export async function getScalingPolicy(serviceId: string): Promise { try { const payload = await requestJson<{ policy?: ScalingPolicy }>( `/scaling/policies/${encodeURIComponent(serviceId)}`, ); return payload.policy ?? null; } catch (error) { if (error instanceof ApiError && error.status === 404) { return null; } throw error; } } export async function setScalingPolicy(policy: ScalingPolicy): Promise { await requestJson('/scaling/policies', { method: 'POST', body: JSON.stringify(policy) }); } export async function deleteScalingPolicy(serviceId: string): Promise { await requestJson(`/scaling/policies/${encodeURIComponent(serviceId)}`, { method: 'DELETE' }); } export async function getServiceScalingState(serviceId: string): Promise { try { const payload = await requestJson<{ state?: ServiceScalingState }>( `/scaling/services/${encodeURIComponent(serviceId)}`, ); return payload.state ?? null; } catch (error) { if (error instanceof ApiError && error.status === 404) { return null; } throw error; } } export async function manualScaleService(serviceId: string, replicas: number, reason = ''): Promise { await requestJson(`/scaling/services/${encodeURIComponent(serviceId)}/scale`, { method: 'POST', body: JSON.stringify({ replicas, reason }), }); } export type FailoverPolicy = components['schemas']['FailoverPolicy']; export type HAStatus = { enabled?: boolean; nodes?: { total?: number; healthy?: number; unhealthy?: number }; health_checks?: { total?: number; healthy?: number; unhealthy?: number }; alerts?: { active?: number }; }; export type HAAlert = { id?: string; rule_id?: string; status?: string; severity?: string; message?: string; starts_at?: string; ends_at?: string; }; export type HAHealthResult = { check_id?: string; status?: string; message?: string; latency?: number; timestamp?: string; }; export async function getHAStatus(): Promise { const payload = await requestJson<{ status?: HAStatus }>('/ha/status'); return payload.status ?? {}; } export async function setHAEnabled(enabled: boolean): Promise { await requestJson(enabled ? '/ha/enable' : '/ha/disable', { method: 'POST' }); } export async function triggerFailover(reason: string): Promise { await requestJson('/ha/failover', { method: 'POST', body: JSON.stringify({ reason }) }); } export async function listFailoverPolicies(): Promise { const payload = await requestJson<{ policies?: FailoverPolicy[] }>('/ha/failover/policies'); return payload.policies ?? []; } export async function setFailoverPolicy(policy: FailoverPolicy): Promise { await requestJson('/ha/failover/policies', { method: 'POST', body: JSON.stringify(policy) }); } export async function deleteFailoverPolicy(serviceId: string): Promise { await requestJson(`/ha/failover/policies/${encodeURIComponent(serviceId)}`, { method: 'DELETE' }); } export async function listActiveAlerts(): Promise { const payload = await requestJson<{ alerts?: HAAlert[] }>('/ha/alerts/active'); return payload.alerts ?? []; } export async function resolveAlert(alertId: string): Promise { await requestJson(`/ha/alerts/${encodeURIComponent(alertId)}/resolve`, { method: 'POST' }); } export async function listHealthResults(): Promise { const payload = await requestJson<{ results?: HAHealthResult[] }>('/ha/health/results'); return payload.results ?? []; } export type SecurityScan = components['schemas']['SecurityScan']; export type Vulnerability = components['schemas']['Vulnerability']; export type SecurityMetrics = { vulnerabilities?: { total?: number; critical?: number; high?: number; medium?: number; low?: number; open?: number; resolved?: number; }; latest_scan?: { id?: string; score?: number; scanned_at?: string; status?: string }; compliance?: { overall_status?: string; score?: number; last_assessed?: string }; security_score?: number; }; export async function startSecurityScan(input: { project_id: string; service_id?: string; scan_type: 'dependency' | 'configuration' | 'comprehensive'; }): Promise { return requestJson('/security/scans', { method: 'POST', body: JSON.stringify(input), }); } export async function getSecurityScan(scanId: string): Promise { const payload = await requestJson<{ scan?: SecurityScan } | SecurityScan>( `/security/scans/${encodeURIComponent(scanId)}`, ); return 'scan' in payload && payload.scan ? payload.scan : (payload as SecurityScan); } export async function getSecurityHistory(projectId: string): Promise { const payload = await requestJson<{ scans?: SecurityScan[] }>( `/projects/${encodeURIComponent(projectId)}/security/history`, ); return payload.scans ?? []; } export async function listVulnerabilities(projectId: string): Promise { const payload = await requestJson<{ vulnerabilities?: Vulnerability[] }>( `/projects/${encodeURIComponent(projectId)}/vulnerabilities`, ); return payload.vulnerabilities ?? []; } export async function updateVulnerability(id: string, status: 'open' | 'resolved' | 'ignored'): Promise { await requestJson(`/vulnerabilities/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ status }), }); } export async function getSecurityMetrics(projectId: string): Promise { return requestJson(`/projects/${encodeURIComponent(projectId)}/security/metrics`); } export type ExecResult = { output?: string; exit_code?: number; error?: string }; export async function execInService(serviceId: string, command: string): Promise { return requestJson(`/services/${encodeURIComponent(serviceId)}/exec`, { method: 'POST', body: JSON.stringify({ command }), }); } // --------------------------------------------------------------------------- // Admin // --------------------------------------------------------------------------- export type AdminUser = { id: string; email: string; name: string; avatar_url?: string; is_admin: boolean; created_at?: string; }; export type AdminOverview = { stats: Record; pending_projects: ProjectEntity[]; }; export async function getAdminOverview(): Promise { const payload = await requestJson<{ stats?: Record; pending_projects?: RawProject[]; }>('/admin/overview'); return { stats: payload.stats ?? {}, pending_projects: (payload.pending_projects ?? []) .map((project) => normalizeProject(project)) .filter((project): project is ProjectEntity => project !== null), }; } export async function listAdminUsers(): Promise { const payload = await requestJson<{ users?: AdminUser[] }>('/admin/users'); return payload.users ?? []; } export async function setUserAdmin(userId: string, isAdmin: boolean): Promise { await requestJson(`/admin/users/${encodeURIComponent(userId)}`, { method: 'PATCH', body: JSON.stringify({ is_admin: isAdmin }), }); } export async function setProjectApproval(projectId: string, isApproved: boolean): Promise { const payload = await requestJson<{ project?: RawProject; message?: string }>( `/admin/projects/${encodeURIComponent(projectId)}`, { method: 'PATCH', body: JSON.stringify({ is_approved: isApproved }) }, ); return payload.project ? normalizeProject(payload.project) : null; }