892 lines
22 KiB
TypeScript
892 lines
22 KiB
TypeScript
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;
|
|
}
|
|
|
|
// --- Hermes telemetry (Phase 3) ---------------------------------------------
|
|
// Per-instance read-only telemetry: sessions, cron, memory/skills, watchdog
|
|
// alerts, backup history. Probe sources (`hermes` CLI, watchdog log, backup
|
|
// repo) may be unavailable on a given host; each section carries its own
|
|
// `status` so the UI can show "definitely empty" vs "couldn't read".
|
|
export type HermesProbeStatus = 'up' | 'down' | 'unknown';
|
|
|
|
export interface HermesSessionStats {
|
|
totalSessions: number;
|
|
totalMessages: number;
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export interface HermesSessionEntry {
|
|
id: string;
|
|
sessionKey: string;
|
|
platform: string | null;
|
|
chatType: string | null;
|
|
displayName: string | null;
|
|
createdAt: string | null;
|
|
updatedAt: string | null;
|
|
suspended: boolean;
|
|
resumePending: boolean;
|
|
totalTokens: number | null;
|
|
estimatedCostUsd: number | null;
|
|
}
|
|
|
|
export interface HermesSessionList {
|
|
entries: HermesSessionEntry[];
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export interface HermesSessionEvent {
|
|
id: string;
|
|
sessionFile: string;
|
|
timestamp: string | null;
|
|
role: string | null;
|
|
eventType: 'message' | 'tool-call' | 'tool-result' | 'reasoning' | 'system' | 'unknown';
|
|
summary: string;
|
|
toolNames: string[];
|
|
itemTypes: string[];
|
|
status: string | null;
|
|
}
|
|
|
|
export interface HermesSessionEventList {
|
|
entries: HermesSessionEvent[];
|
|
status: HermesProbeStatus;
|
|
sourceCount: number;
|
|
}
|
|
|
|
export interface HermesCronEntry {
|
|
id: string;
|
|
name: string;
|
|
schedule: string | null;
|
|
lastRun: string | null;
|
|
nextRun: string | null;
|
|
lastStatus: string | null;
|
|
active: boolean;
|
|
}
|
|
|
|
export interface HermesCronList {
|
|
entries: HermesCronEntry[];
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export interface HermesMemoryItem {
|
|
id: string;
|
|
type: string;
|
|
key: string;
|
|
summary: string;
|
|
updatedAt: string | null;
|
|
}
|
|
|
|
export interface HermesMemoryList {
|
|
items: HermesMemoryItem[];
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export interface HermesSkillItem {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface HermesSkillList {
|
|
items: HermesSkillItem[];
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export type HermesWatchdogSeverity = 'info' | 'warn' | 'critical';
|
|
|
|
export interface HermesWatchdogAlert {
|
|
timestamp: string;
|
|
severity: HermesWatchdogSeverity;
|
|
message: string;
|
|
}
|
|
|
|
export interface HermesWatchdogFeed {
|
|
alerts: HermesWatchdogAlert[];
|
|
source: string | null;
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export interface HermesBackupHistoryEntry {
|
|
sha: string;
|
|
committedAt: string;
|
|
subject: string;
|
|
}
|
|
|
|
export interface HermesBackupHistory {
|
|
entries: HermesBackupHistoryEntry[];
|
|
repoPath: string | null;
|
|
status: HermesProbeStatus;
|
|
}
|
|
|
|
export interface HermesTelemetrySnapshot {
|
|
generatedAt: string;
|
|
cached: boolean;
|
|
instanceId: 'vijay' | 'bheem';
|
|
sessions: HermesSessionStats;
|
|
sessionList: HermesSessionList;
|
|
sessionEvents: HermesSessionEventList;
|
|
cron: HermesCronList;
|
|
memory: HermesMemoryList;
|
|
skills: HermesSkillList;
|
|
watchdog: HermesWatchdogFeed;
|
|
backupHistory: HermesBackupHistory;
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface HermesTokenSummary {
|
|
sessionCount: number;
|
|
messageCount: number;
|
|
toolCallCount: number;
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
cacheReadTokens: number;
|
|
cacheWriteTokens: number;
|
|
reasoningTokens: number;
|
|
totalTokens: number;
|
|
estimatedCostUsd: number;
|
|
actualCostUsd: number;
|
|
firstStartedAt: string | null;
|
|
lastActivityAt: string | null;
|
|
}
|
|
|
|
export interface HermesTokenByModel {
|
|
model: string;
|
|
provider: string;
|
|
sessionCount: number;
|
|
totalTokens: number;
|
|
estimatedCostUsd: number;
|
|
}
|
|
|
|
export interface HermesTokenByDay {
|
|
day: string;
|
|
sessionCount: number;
|
|
totalTokens: number;
|
|
estimatedCostUsd: number;
|
|
}
|
|
|
|
export interface HermesTokenSession {
|
|
id: string;
|
|
title: string | null;
|
|
source: string | null;
|
|
model: string;
|
|
provider: string;
|
|
startedAt: string | null;
|
|
endedAt: string | null;
|
|
totalTokens: number;
|
|
messageCount: number;
|
|
toolCallCount: number;
|
|
estimatedCostUsd: number;
|
|
}
|
|
|
|
export interface HermesTokenUsageSnapshot {
|
|
generatedAt: string;
|
|
cached: boolean;
|
|
instanceId: 'vijay' | 'bheem';
|
|
status: HermesProbeStatus;
|
|
source: string | null;
|
|
summary: HermesTokenSummary;
|
|
byModel: HermesTokenByModel[];
|
|
byDay: HermesTokenByDay[];
|
|
recentSessions: HermesTokenSession[];
|
|
warnings: 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<string | null> {
|
|
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<string | null> {
|
|
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<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const 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<Service[]>('/api/services'),
|
|
getService: (id: string) => apiRequest<Service>(`/api/services/${id}`),
|
|
createService: (data: Partial<Service>) =>
|
|
apiRequest<Service>('/api/services', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateService: (id: string, data: Partial<Service>) =>
|
|
apiRequest<Service>(`/api/services/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteService: (id: string) =>
|
|
apiRequest<void>(`/api/services/${id}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// Deployments
|
|
getDeployments: (limit = 20) => apiRequest<Deployment[]>(`/api/deployments?limit=${limit}`),
|
|
getServiceDeployments: (serviceId: string, limit = 50) =>
|
|
apiRequest<Deployment[]>(`/api/deployments/service/${serviceId}?limit=${limit}`),
|
|
getDeployment: (id: string) => apiRequest<Deployment>(`/api/deployments/${id}`),
|
|
getDeploymentLogs: (id: string) =>
|
|
apiRequest<DeploymentLogsResponse>(`/api/deployments/${id}/logs`),
|
|
triggerDeployment: (serviceId: string) =>
|
|
apiRequest<{ deploymentId: string; status: string }>(`/api/deployments/trigger/${serviceId}`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
// Health
|
|
getHealth: () => apiRequest<ServiceHealth[]>('/api/health'),
|
|
getServiceHealth: (serviceId: string) =>
|
|
apiRequest<ServiceHealth>(`/api/health/${serviceId}`),
|
|
clearHealthCache: () => apiRequest<{ message: string }>('/api/health/cache', { method: 'DELETE' }),
|
|
|
|
// Hermes operations
|
|
getHermesOps: () => apiRequest<HermesOpsSnapshot>('/api/hermes/ops'),
|
|
|
|
// Hermes per-instance telemetry (Phase 3 — sessions/cron/memory/skills/
|
|
// watchdog/backup-history). Returns a Zod-validated snapshot from the
|
|
// backend; sections may report status:'unknown' if their underlying
|
|
// source isn't readable in the current environment (CI / dev box).
|
|
getHermesTelemetry: (instance: 'vijay' | 'bheem') =>
|
|
apiRequest<HermesTelemetrySnapshot>(`/api/hermes/telemetry/${instance}`),
|
|
getHermesTokenUsage: (instance: 'vijay' | 'bheem') =>
|
|
apiRequest<HermesTokenUsageSnapshot>(`/api/hermes/telemetry/${instance}/token-usage`),
|
|
|
|
// Seed
|
|
seedServices: () => apiRequest<{ message: string }>('/api/seed', { method: 'POST' }),
|
|
|
|
// Environment Variables
|
|
getEnvVars: () => apiRequest<EnvVar[]>('/api/env'),
|
|
getEnvVar: (id: string) => apiRequest<EnvVar>(`/api/env/${id}`),
|
|
createEnvVar: (data: Partial<EnvVar>) =>
|
|
apiRequest<EnvVar>('/api/env', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateEnvVar: (id: string, data: Partial<EnvVar>) =>
|
|
apiRequest<EnvVar>(`/api/env/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteEnvVar: (id: string) =>
|
|
apiRequest<void>(`/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<EnvVar>) => api.createEnvVar(data);
|
|
export const updateEnvVar = (id: string, data: Partial<EnvVar>) => 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<AzureConfig>('/api/azure-config'),
|
|
createAzureConfig: (data: Partial<AzureConfig>) =>
|
|
apiRequest<AzureConfig>('/api/azure-config', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateAzureConfig: (id: string, data: Partial<AzureConfig>) =>
|
|
apiRequest<AzureConfig>(`/api/azure-config/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteAzureConfig: (id: string) =>
|
|
apiRequest<void>(`/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<AzureConfig>) => azureApi.createAzureConfig(data);
|
|
export const updateAzureConfig = (id: string, data: Partial<AzureConfig>) => 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<CodeQualityReport>('/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<string, VmCheck>;
|
|
error?: string;
|
|
}
|
|
|
|
export interface CronRunSummary {
|
|
timestamp: string;
|
|
mode: 'standard' | 'full';
|
|
diskBefore: string;
|
|
diskAfter: string;
|
|
freedMB: number;
|
|
durationSecs: number;
|
|
success: boolean;
|
|
steps: string[];
|
|
jsonSummary?: Record<string, unknown>;
|
|
}
|
|
|
|
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<VmHealthResult>('/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<CronStatusResponse>('/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<OllamaModelsResponse>('/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<TrendSeries>(`/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<ContainerInfo[]>('/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<LoginResponse> => {
|
|
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<RefreshResponse> => {
|
|
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<MeResponse> => {
|
|
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<string | null> {
|
|
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;
|
|
}
|
|
}
|