import { devopsApiUrl, platformUrl } from './product-config'; // Platform service URL for auth const PLATFORM_SERVICE_URL = platformUrl; export interface Service { id: string; name: string; scriptPath: string; healthUrl: string; repoPath: string; status: 'up' | 'down' | 'degraded'; version: string; lastDeployedAt?: string; lastHealthCheckAt?: string; productId: string; } export interface Deployment { id: string; serviceId: string; version: string; status: 'running' | 'success' | 'failed'; logs: string; triggeredBy: string; triggeredAt: string; completedAt?: string; productId: string; } export interface ServiceHealth { serviceId: string; status: 'up' | 'down' | 'degraded'; responseTime?: number; lastCheck: string; } export interface ApiError { error: string; status?: number; } export interface DeploymentLogsResponse { logs: string; status: 'running' | 'success' | 'failed'; } export interface EnvVar { id: string; name: string; value: string; isSecret: boolean; source: 'local' | 'azure-key-vault'; azureKeyVaultName?: string; azureSecretName?: string; updatedAt: string; } export interface HermesOpsTimer { name: string; active: boolean; nextRun: string | null; lastRun: string | null; } export interface HermesOpsRepo { path: string; branch: string | null; clean: boolean; head: string | null; lastCommitAt: string | null; size: string | null; } export interface HermesOpsInstance { id: 'vijay' | 'bheem'; label: string; hermesHome: string; gateway: { service: string; active: boolean; enabled: boolean; }; dashboard: { service: string; active: boolean; url: string; }; backup: { timer: HermesOpsTimer; repo: HermesOpsRepo; restoredFileCount: number | null; restoredCronJobs: number | null; }; google: { workspaceToken: boolean; driveFolder: string; }; } export interface HermesOpsSessionSummary { active: number; updatedAt: string | null; } export interface HermesOpsCronJob { name: string; label: string; active: boolean; nextRun: string | null; lastRun: string | null; } export interface HermesOpsLink { label: string; href: string; description: string; } export interface HermesOpsSnapshot { generatedAt: string; tailscaleIp: string | null; emergencyDriveUpload: HermesOpsTimer; activeSessions: HermesOpsSessionSummary; cronJobs: HermesOpsCronJob[]; recentAlerts: string[]; quickLinks: HermesOpsLink[]; instances: HermesOpsInstance[]; warnings: string[]; } let csrfToken: string | null = null; let csrfTokenExpiresAt: number = 0; async function getAccessToken(): Promise { if (typeof window === 'undefined') return null; let token = getAccessTokenFromStorage(); // If no token, try to refresh if (!token) { token = await refreshAccessToken(); } return token; } async function getCsrfToken(): Promise { if (csrfToken && Date.now() < csrfTokenExpiresAt) { return csrfToken; } try { const token = await getAccessToken(); if (!token) { return null; } const response = await fetch(`${devopsApiUrl}/api/csrf-token`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (response.ok) { const data = await response.json(); csrfToken = data.csrfToken; csrfTokenExpiresAt = Date.now() + 3000000; // 50 minutes (before 1 hour expiry) return csrfToken; } } catch (error) { console.error('Failed to fetch CSRF token:', error); } return null; } export async function apiRequest( endpoint: string, options: RequestInit = {} ): Promise { let token = await getAccessToken(); const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const method = options.method?.toUpperCase(); const stateChangingMethods = ['POST', 'PUT', 'DELETE', 'PATCH']; if (method && stateChangingMethods.includes(method)) { const csrf = await getCsrfToken(); if (csrf) { headers['X-CSRF-Token'] = csrf; } } let response = await fetch(`${devopsApiUrl}${endpoint}`, { ...options, headers, }); // Handle 401 - try to refresh token and retry if (response.status === 401 && token) { const newToken = await refreshAccessToken(); if (newToken) { headers['Authorization'] = `Bearer ${newToken}`; response = await fetch(`${devopsApiUrl}${endpoint}`, { ...options, headers, }); } } // Handle 403 - CSRF token retry if (response.status === 403) { const errorData = await response.json().catch(() => ({})); if (errorData.error === 'Invalid CSRF token') { csrfToken = null; csrfTokenExpiresAt = 0; const newCsrf = await getCsrfToken(); if (newCsrf) { headers['X-CSRF-Token'] = newCsrf; response = await fetch(`${devopsApiUrl}${endpoint}`, { ...options, headers, }); } } } if (!response.ok) { const error: ApiError = { error: `API error: ${response.status} ${response.statusText}`, status: response.status }; throw error; } return response.json(); } export const api = { // Services getServices: () => apiRequest('/api/services'), getService: (id: string) => apiRequest(`/api/services/${id}`), createService: (data: Partial) => apiRequest('/api/services', { method: 'POST', body: JSON.stringify(data), }), updateService: (id: string, data: Partial) => apiRequest(`/api/services/${id}`, { method: 'PUT', body: JSON.stringify(data), }), deleteService: (id: string) => apiRequest(`/api/services/${id}`, { method: 'DELETE', }), // Deployments getDeployments: (limit = 20) => apiRequest(`/api/deployments?limit=${limit}`), getServiceDeployments: (serviceId: string, limit = 50) => apiRequest(`/api/deployments/service/${serviceId}?limit=${limit}`), getDeployment: (id: string) => apiRequest(`/api/deployments/${id}`), getDeploymentLogs: (id: string) => apiRequest(`/api/deployments/${id}/logs`), triggerDeployment: (serviceId: string) => apiRequest<{ deploymentId: string; status: string }>(`/api/deployments/trigger/${serviceId}`, { method: 'POST', }), // Health getHealth: () => apiRequest('/api/health'), getServiceHealth: (serviceId: string) => apiRequest(`/api/health/${serviceId}`), clearHealthCache: () => apiRequest<{ message: string }>('/api/health/cache', { method: 'DELETE' }), // Hermes operations getHermesOps: () => apiRequest('/api/hermes/ops'), // Seed seedServices: () => apiRequest<{ message: string }>('/api/seed', { method: 'POST' }), // Environment Variables getEnvVars: () => apiRequest('/api/env'), getEnvVar: (id: string) => apiRequest(`/api/env/${id}`), createEnvVar: (data: Partial) => apiRequest('/api/env', { method: 'POST', body: JSON.stringify(data), }), updateEnvVar: (id: string, data: Partial) => apiRequest(`/api/env/${id}`, { method: 'PUT', body: JSON.stringify(data), }), deleteEnvVar: (id: string) => apiRequest(`/api/env/${id}`, { method: 'DELETE', }), syncAzureKeyVault: () => apiRequest<{ synced: number; errors: string[] }>('/api/env/sync-azure', { method: 'POST', }), }; // Standalone functions for environment variables (used by env page) export const getEnvVars = () => api.getEnvVars(); export const createEnvVar = (data: Partial) => api.createEnvVar(data); export const updateEnvVar = (id: string, data: Partial) => api.updateEnvVar(id, data); export const deleteEnvVar = (id: string) => api.deleteEnvVar(id); // Azure Config export interface AzureConfig { id: string; tenantId: string; clientId: string; keyVaultUrl: string; isActive: boolean; updatedAt: string; hasClientSecret?: boolean; } export const azureApi = { getAzureConfig: () => apiRequest('/api/azure-config'), createAzureConfig: (data: Partial) => apiRequest('/api/azure-config', { method: 'POST', body: JSON.stringify(data), }), updateAzureConfig: (id: string, data: Partial) => apiRequest(`/api/azure-config/${id}`, { method: 'PUT', body: JSON.stringify(data), }), deleteAzureConfig: (id: string) => apiRequest(`/api/azure-config/${id}`, { method: 'DELETE', }), testAzureConnection: () => apiRequest<{ success: boolean; error?: string }>('/api/azure-config/test', { method: 'POST', }), }; export const getAzureConfig = () => azureApi.getAzureConfig(); export const createAzureConfig = (data: Partial) => azureApi.createAzureConfig(data); export const updateAzureConfig = (id: string, data: Partial) => azureApi.updateAzureConfig(id, data); export const deleteAzureConfig = (id: string) => azureApi.deleteAzureConfig(id); export const testAzureConnection = () => azureApi.testAzureConnection(); // Code Quality export interface CodeQualityIssue { id: string; type: 'error' | 'warning' | 'info'; category: 'typescript' | 'eslint' | 'build' | 'test' | 'format'; file: string; line?: number; column?: number; message: string; rule?: string; } export interface CodeQualityReport { id: string; projectId: string; projectName: string; projectPath: string; timestamp: string; summary: { totalIssues: number; errors: number; warnings: number; infos: number; }; categories: { typescript: { errors: number; warnings: number; duration: number; }; eslint: { errors: number; warnings: number; duration: number; }; build: { success: boolean; duration: number; errors: number; }; test: { success: boolean; passed: number; failed: number; duration: number; }; }; issues: CodeQualityIssue[]; } export interface CodeQualityCheckParams { projectId: string; projectPath: string; checks: Array<'typescript' | 'eslint' | 'build' | 'test'>; } export const codeQualityApi = { runCheck: (params: CodeQualityCheckParams) => apiRequest('/api/code-quality/check', { method: 'POST', body: JSON.stringify(params), }), }; export const runCodeQualityCheck = (params: CodeQualityCheckParams) => codeQualityApi.runCheck(params); // VM Health export type VmCheckLevel = 'OK' | 'WARN' | 'CRIT'; export interface VmCheck { level: VmCheckLevel; value: string; message: string; } export interface VmHealthResult { timestamp: string; hostname: string; overall: VmCheckLevel; checks: Record; error?: string; } export interface CronRunSummary { timestamp: string; mode: 'standard' | 'full'; diskBefore: string; diskAfter: string; freedMB: number; durationSecs: number; success: boolean; steps: string[]; jsonSummary?: Record; } export interface CronJob { name: string; schedule: string; description: string; lastRun: CronRunSummary | null; nextRun: string | null; } export interface CronStatusResponse { jobs: CronJob[]; recentRuns: CronRunSummary[]; } export interface UnhealthyContainer { name: string; status: string; restartCount: number; lastHealthLogs: string[]; unhealthySince: string | null; } export interface OllamaModel { name: string; sizeGB: number; modifiedAt: string; } export interface OllamaRunning { name: string; sizeGB: number; processor: string; expiresAt: string; } export interface OllamaModelsResponse { models: OllamaModel[]; running: OllamaRunning[]; } export interface ContainerInfo { name: string; image: string; stack: string; state: string; health: string; uptimeSecs: number; cpuPercent: number; memMiB: number; memLimitMiB: number; restartCount: number; } export const vmApi = { getHealth: () => apiRequest('/api/vm/health'), getCleanupLog: (lines = 40) => apiRequest<{ log: string }>(`/api/vm/cleanup-log?lines=${lines}`), runCleanup: (mode: 'weekly' | 'monthly' | 'dry-run') => apiRequest<{ success: boolean; output: string }>('/api/vm/cleanup', { method: 'POST', body: JSON.stringify({ mode }), }), getCronStatus: () => apiRequest('/api/vm/cron-status'), getUnhealthyContainers: () => apiRequest<{ containers: UnhealthyContainer[] }>('/api/vm/containers/unhealthy'), restartContainer: (name: string) => apiRequest<{ success: boolean; message: string }>( `/api/vm/containers/${encodeURIComponent(name)}/restart`, { method: 'POST' }, ), getOllamaModels: () => apiRequest('/api/vm/ollama/models'), unloadOllamaModel: (name: string) => apiRequest<{ success: boolean; message: string }>( `/api/vm/ollama/models/${encodeURIComponent(name)}`, { method: 'DELETE' }, ), getTrend: (metric: 'disk' | 'steal' | 'io', range: '7d' | '30d') => apiRequest(`/api/vm/metrics/trend?metric=${metric}&range=${range}`), getMemoryTrend: (range: '7d' | '30d') => apiRequest<{ available: TrendSeries; swap: TrendSeries }>( `/api/vm/metrics/trend?metric=memory&range=${range}`, ), getAllContainers: () => apiRequest('/api/vm/containers'), getContainerLogs: (name: string, lines = 50) => apiRequest<{ logs: string }>(`/api/vm/containers/${encodeURIComponent(name)}/logs?lines=${lines}`), }; export interface TrendPoint { t: number; v: number } export interface TrendSeries { metric: string; unit: string; points: TrendPoint[]; latest: number; avg: number; peak: number; } // Auth API - calls platform-service for authentication export interface LoginRequest { email: string; password: string; productId: string; } export interface LoginResponse { accessToken: string; refreshToken: string; user: { id: string; email: string; role: string; plan: string; displayName: string; products?: Array<{ productId: string; plan: string; role: string; }>; }; } export interface RefreshRequest { refreshToken: string; } export interface RefreshResponse { accessToken: string; refreshToken: string; } export interface MeResponse { id: string; email: string; role: string; plan: string; displayName: string; emailVerified: boolean; currentProduct: string; products: Array<{ productId: string; plan: string; role: string; }>; mfaEnabled: boolean; mfaMethods: string[]; } export const authApi = { login: async (data: LoginRequest): Promise => { const response = await fetch(`${PLATFORM_SERVICE_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Login failed' })); throw new Error(error.error || 'Login failed'); } return response.json(); }, refresh: async (data: RefreshRequest): Promise => { const response = await fetch(`${PLATFORM_SERVICE_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error('Token refresh failed'); } return response.json(); }, me: async (token: string): Promise => { const response = await fetch(`${PLATFORM_SERVICE_URL}/auth/me`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error('Failed to get user info'); } return response.json(); }, }; // Helper functions for auth state management export function setAccessToken(token: string): void { if (typeof window !== 'undefined') { localStorage.setItem('access_token', token); } } export function setRefreshToken(token: string): void { if (typeof window !== 'undefined') { localStorage.setItem('refresh_token', token); } } export function getAccessTokenFromStorage(): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem('access_token'); } export function getRefreshTokenFromStorage(): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem('refresh_token'); } export function clearAuthTokens(): void { if (typeof window !== 'undefined') { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); } } export async function refreshAccessToken(): Promise { const refreshToken = getRefreshTokenFromStorage(); if (!refreshToken) return null; try { const response = await authApi.refresh({ refreshToken }); setAccessToken(response.accessToken); setRefreshToken(response.refreshToken); return response.accessToken; } catch { clearAuthTokens(); return null; } }