First swing.

This commit is contained in:
2025-10-12 17:44:54 +00:00
parent 33454bc4e1
commit e5bd3c36f5
106 changed files with 7592 additions and 1270 deletions

54
src/services/api/apps.ts Normal file
View File

@@ -0,0 +1,54 @@
import { apiClient } from './client';
import type {
AppListResponse,
App,
AppAddRequest,
AppAddResponse,
AppStatus,
OperationResponse,
} from './types';
export const appsApi = {
// Available apps (from catalog)
async listAvailable(): Promise<AppListResponse> {
return apiClient.get('/api/v1/apps');
},
async getAvailable(appName: string): Promise<App> {
return apiClient.get(`/api/v1/apps/${appName}`);
},
// Deployed apps (instance-specific)
async listDeployed(instanceName: string): Promise<AppListResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/apps`);
},
async add(instanceName: string, app: AppAddRequest): Promise<AppAddResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/apps`, app);
},
async deploy(instanceName: string, appName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/deploy`);
},
async delete(instanceName: string, appName: string): Promise<OperationResponse> {
return apiClient.delete(`/api/v1/instances/${instanceName}/apps/${appName}`);
},
async getStatus(instanceName: string, appName: string): Promise<AppStatus> {
return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/status`);
},
// Backup operations
async backup(instanceName: string, appName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/backup`);
},
async listBackups(instanceName: string, appName: string): Promise<{ backups: Array<{ id: string; timestamp: string; size?: string }> }> {
return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/backup`);
},
async restore(instanceName: string, appName: string, backupId: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/restore`, { backup_id: backupId });
},
};

122
src/services/api/client.ts Normal file
View File

@@ -0,0 +1,122 @@
export class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public details?: unknown
) {
super(message);
this.name = 'ApiError';
}
}
export class ApiClient {
constructor(private baseUrl: string = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055') {}
private async request<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
return response.json();
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
error instanceof Error ? error.message : 'Network error',
0
);
}
}
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' });
}
async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
});
}
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
async getText(endpoint: string): Promise<string> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
return response.text();
}
async putText(endpoint: string, text: string): Promise<{ message?: string; [key: string]: unknown }> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
body: text,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
return response.json();
}
}
export const apiClient = new ApiClient();

View File

@@ -0,0 +1,47 @@
import { apiClient } from './client';
import type {
ClusterConfig,
ClusterStatus,
ClusterHealthResponse,
KubeconfigResponse,
TalosconfigResponse,
OperationResponse,
} from './types';
export const clusterApi = {
async generateConfig(instanceName: string, config: ClusterConfig): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/cluster/config/generate`, config);
},
async bootstrap(instanceName: string, node: string): Promise<OperationResponse> {
return apiClient.post<OperationResponse>(`/api/v1/instances/${instanceName}/cluster/bootstrap`, { node });
},
async configureEndpoints(instanceName: string, includeNodes = false): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/cluster/endpoints`, { include_nodes: includeNodes });
},
async getStatus(instanceName: string): Promise<ClusterStatus> {
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/status`);
},
async getHealth(instanceName: string): Promise<ClusterHealthResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/health`);
},
async getKubeconfig(instanceName: string): Promise<KubeconfigResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/kubeconfig`);
},
async generateKubeconfig(instanceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/cluster/kubeconfig/generate`);
},
async getTalosconfig(instanceName: string): Promise<TalosconfigResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/talosconfig`);
},
async reset(instanceName: string, confirm: boolean): Promise<OperationResponse> {
return apiClient.post<OperationResponse>(`/api/v1/instances/${instanceName}/cluster/reset`, { confirm });
},
};

View File

@@ -0,0 +1,12 @@
import { apiClient } from './client';
import type { ContextResponse, SetContextResponse } from './types';
export const contextApi = {
async get(): Promise<ContextResponse> {
return apiClient.get('/api/v1/context');
},
async set(context: string): Promise<SetContextResponse> {
return apiClient.post<SetContextResponse>('/api/v1/context', { context });
},
};

View File

@@ -0,0 +1,28 @@
import { apiClient } from './client';
export interface DnsmasqStatus {
running: boolean;
status?: string;
}
export const dnsmasqApi = {
async getStatus(): Promise<DnsmasqStatus> {
return apiClient.get('/api/v1/dnsmasq/status');
},
async getConfig(): Promise<string> {
return apiClient.getText('/api/v1/dnsmasq/config');
},
async restart(): Promise<{ message: string }> {
return apiClient.post('/api/v1/dnsmasq/restart');
},
async generate(): Promise<{ message: string }> {
return apiClient.post('/api/v1/dnsmasq/generate');
},
async update(): Promise<{ message: string }> {
return apiClient.post('/api/v1/dnsmasq/update');
},
};

View File

@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import { clusterApi, nodesApi } from '..';
import type { ClusterHealthResponse, ClusterStatus, NodeListResponse } from '../types';
export const useClusterHealth = (instanceName: string) => {
return useQuery<ClusterHealthResponse>({
queryKey: ['cluster-health', instanceName],
queryFn: () => clusterApi.getHealth(instanceName),
enabled: !!instanceName,
refetchInterval: 10000, // Auto-refresh every 10 seconds
staleTime: 5000,
});
};
export const useClusterStatus = (instanceName: string) => {
return useQuery<ClusterStatus>({
queryKey: ['cluster-status', instanceName],
queryFn: () => clusterApi.getStatus(instanceName),
enabled: !!instanceName,
refetchInterval: 10000, // Auto-refresh every 10 seconds
staleTime: 5000,
});
};
export const useClusterNodes = (instanceName: string) => {
return useQuery<NodeListResponse>({
queryKey: ['cluster-nodes', instanceName],
queryFn: () => nodesApi.list(instanceName),
enabled: !!instanceName,
refetchInterval: 10000, // Auto-refresh every 10 seconds
staleTime: 5000,
});
};

View File

@@ -0,0 +1,40 @@
import { useQuery } from '@tanstack/react-query';
import { instancesApi, operationsApi, clusterApi } from '..';
import type { GetInstanceResponse, OperationListResponse, ClusterHealthResponse } from '../types';
export const useInstance = (name: string) => {
return useQuery<GetInstanceResponse>({
queryKey: ['instance', name],
queryFn: () => instancesApi.get(name),
enabled: !!name,
staleTime: 30000, // 30 seconds
});
};
export const useInstanceOperations = (instanceName: string, limit?: number) => {
return useQuery<OperationListResponse>({
queryKey: ['instance-operations', instanceName],
queryFn: async () => {
const response = await operationsApi.list(instanceName);
if (limit) {
return {
operations: response.operations.slice(0, limit)
};
}
return response;
},
enabled: !!instanceName,
refetchInterval: 3000, // Poll every 3 seconds
staleTime: 1000,
});
};
export const useInstanceClusterHealth = (instanceName: string) => {
return useQuery<ClusterHealthResponse>({
queryKey: ['instance-cluster-health', instanceName],
queryFn: () => clusterApi.getHealth(instanceName),
enabled: !!instanceName,
refetchInterval: 10000, // Refresh every 10 seconds
staleTime: 5000,
});
};

View File

@@ -0,0 +1,58 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { operationsApi } from '../operations';
import type { OperationListResponse, Operation } from '../types';
export const useOperations = (instanceName: string, filter?: 'running' | 'completed' | 'failed') => {
return useQuery<OperationListResponse>({
queryKey: ['operations', instanceName, filter],
queryFn: async () => {
const response = await operationsApi.list(instanceName);
if (filter) {
const filtered = response.operations.filter(op => {
if (filter === 'running') return op.status === 'running' || op.status === 'pending';
if (filter === 'completed') return op.status === 'completed';
if (filter === 'failed') return op.status === 'failed';
return true;
});
return { operations: filtered };
}
return response;
},
enabled: !!instanceName,
refetchInterval: 3000, // Poll every 3 seconds for real-time updates
staleTime: 1000,
});
};
export const useOperation = (operationId: string) => {
return useQuery<Operation>({
queryKey: ['operation', operationId],
queryFn: () => operationsApi.get(operationId),
enabled: !!operationId,
refetchInterval: (query) => {
// Stop polling if operation is completed, failed, or cancelled
const status = query.state.data?.status;
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
return false;
}
return 2000; // Poll every 2 seconds while running
},
staleTime: 1000,
});
};
export const useCancelOperation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ operationId, instanceName }: { operationId: string; instanceName: string }) =>
operationsApi.cancel(operationId, instanceName),
onSuccess: (_, { operationId }) => {
// Invalidate operation queries to refresh data
queryClient.invalidateQueries({ queryKey: ['operation', operationId] });
queryClient.invalidateQueries({ queryKey: ['operations'] });
},
});
};

View File

@@ -0,0 +1,57 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { pxeApi } from '../pxe';
import type { DownloadAssetRequest, PxeAssetType } from '../types';
export function usePxeAssets(instanceName: string | null | undefined) {
return useQuery({
queryKey: ['instances', instanceName, 'pxe', 'assets'],
queryFn: () => pxeApi.listAssets(instanceName!),
enabled: !!instanceName,
refetchInterval: 5000, // Poll every 5 seconds to track download status
});
}
export function usePxeAsset(
instanceName: string | null | undefined,
assetType: PxeAssetType | null | undefined
) {
return useQuery({
queryKey: ['instances', instanceName, 'pxe', 'assets', assetType],
queryFn: () => pxeApi.getAsset(instanceName!, assetType!),
enabled: !!instanceName && !!assetType,
});
}
export function useDownloadPxeAsset() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
instanceName,
request,
}: {
instanceName: string;
request: DownloadAssetRequest;
}) => pxeApi.downloadAsset(instanceName, request),
onSuccess: (_data, variables) => {
// Invalidate assets list to show downloading status
queryClient.invalidateQueries({
queryKey: ['instances', variables.instanceName, 'pxe', 'assets'],
});
},
});
}
export function useDeletePxeAsset() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ instanceName, type }: { instanceName: string; type: PxeAssetType }) =>
pxeApi.deleteAsset(instanceName, type),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: ['instances', variables.instanceName, 'pxe', 'assets'],
});
},
});
}

View File

@@ -0,0 +1,47 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { utilitiesApi } from '../utilities';
export function useDashboardToken() {
return useQuery({
queryKey: ['utilities', 'dashboard', 'token'],
queryFn: utilitiesApi.getDashboardToken,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useClusterVersions() {
return useQuery({
queryKey: ['utilities', 'version'],
queryFn: utilitiesApi.getVersion,
staleTime: 10 * 60 * 1000, // 10 minutes
});
}
export function useNodeIPs() {
return useQuery({
queryKey: ['utilities', 'nodes', 'ips'],
queryFn: utilitiesApi.getNodeIPs,
staleTime: 30 * 1000, // 30 seconds
});
}
export function useControlPlaneIP() {
return useQuery({
queryKey: ['utilities', 'controlplane', 'ip'],
queryFn: utilitiesApi.getControlPlaneIP,
staleTime: 60 * 1000, // 1 minute
});
}
export function useCopySecret() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ secret, targetInstance }: { secret: string; targetInstance: string }) =>
utilitiesApi.copySecret(secret, targetInstance),
onSuccess: () => {
// Invalidate secrets queries
queryClient.invalidateQueries({ queryKey: ['secrets'] });
},
});
}

19
src/services/api/index.ts Normal file
View File

@@ -0,0 +1,19 @@
export { apiClient, ApiError } from './client';
export * from './types';
export { instancesApi } from './instances';
export { contextApi } from './context';
export { nodesApi } from './nodes';
export { clusterApi } from './cluster';
export { appsApi } from './apps';
export { servicesApi } from './services';
export { operationsApi } from './operations';
export { dnsmasqApi } from './dnsmasq';
export { utilitiesApi } from './utilities';
export { pxeApi } from './pxe';
// React Query hooks
export { useInstance, useInstanceOperations, useInstanceClusterHealth } from './hooks/useInstance';
export { useOperations, useOperation, useCancelOperation } from './hooks/useOperations';
export { useClusterHealth, useClusterStatus, useClusterNodes } from './hooks/useCluster';
export { useDashboardToken, useClusterVersions, useNodeIPs, useControlPlaneIP, useCopySecret } from './hooks/useUtilities';
export { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from './hooks/usePxeAssets';

View File

@@ -0,0 +1,49 @@
import { apiClient } from './client';
import type {
InstanceListResponse,
CreateInstanceRequest,
CreateInstanceResponse,
DeleteInstanceResponse,
GetInstanceResponse,
} from './types';
export const instancesApi = {
async list(): Promise<InstanceListResponse> {
return apiClient.get('/api/v1/instances');
},
async get(name: string): Promise<GetInstanceResponse> {
return apiClient.get(`/api/v1/instances/${name}`);
},
async create(data: CreateInstanceRequest): Promise<CreateInstanceResponse> {
return apiClient.post('/api/v1/instances', data);
},
async delete(name: string): Promise<DeleteInstanceResponse> {
return apiClient.delete(`/api/v1/instances/${name}`);
},
// Config management
async getConfig(instanceName: string): Promise<Record<string, unknown>> {
return apiClient.get(`/api/v1/instances/${instanceName}/config`);
},
async updateConfig(instanceName: string, config: Record<string, unknown>): Promise<{ message: string }> {
return apiClient.put(`/api/v1/instances/${instanceName}/config`, config);
},
async batchUpdateConfig(instanceName: string, updates: Array<{path: string; value: unknown}>): Promise<{ message: string; updated?: number }> {
return apiClient.patch(`/api/v1/instances/${instanceName}/config`, { updates });
},
// Secrets management
async getSecrets(instanceName: string, raw = false): Promise<Record<string, unknown>> {
const query = raw ? '?raw=true' : '';
return apiClient.get(`/api/v1/instances/${instanceName}/secrets${query}`);
},
async updateSecrets(instanceName: string, secrets: Record<string, unknown>): Promise<{ message: string }> {
return apiClient.put(`/api/v1/instances/${instanceName}/secrets`, secrets);
},
};

57
src/services/api/nodes.ts Normal file
View File

@@ -0,0 +1,57 @@
import { apiClient } from './client';
import type {
NodeListResponse,
NodeAddRequest,
NodeUpdateRequest,
Node,
HardwareInfo,
DiscoveryStatus,
OperationResponse,
} from './types';
export const nodesApi = {
async list(instanceName: string): Promise<NodeListResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/nodes`);
},
async get(instanceName: string, nodeName: string): Promise<Node> {
return apiClient.get(`/api/v1/instances/${instanceName}/nodes/${nodeName}`);
},
async add(instanceName: string, node: NodeAddRequest): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/nodes`, node);
},
async update(instanceName: string, nodeName: string, updates: NodeUpdateRequest): Promise<OperationResponse> {
return apiClient.put(`/api/v1/instances/${instanceName}/nodes/${nodeName}`, updates);
},
async delete(instanceName: string, nodeName: string): Promise<OperationResponse> {
return apiClient.delete(`/api/v1/instances/${instanceName}/nodes/${nodeName}`);
},
async apply(instanceName: string, nodeName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/${nodeName}/apply`);
},
// Discovery
async discover(instanceName: string, subnet: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/discover`, { subnet });
},
async detect(instanceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/detect`);
},
async discoveryStatus(instanceName: string): Promise<DiscoveryStatus> {
return apiClient.get(`/api/v1/instances/${instanceName}/discovery`);
},
async getHardware(instanceName: string, ip: string): Promise<HardwareInfo> {
return apiClient.get(`/api/v1/instances/${instanceName}/nodes/hardware/${ip}`);
},
async fetchTemplates(instanceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/fetch-templates`);
},
};

View File

@@ -0,0 +1,23 @@
import { apiClient } from './client';
import type { Operation, OperationListResponse } from './types';
export const operationsApi = {
async list(instanceName: string): Promise<OperationListResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/operations`);
},
async get(operationId: string, instanceName?: string): Promise<Operation> {
const params = instanceName ? `?instance=${instanceName}` : '';
return apiClient.get(`/api/v1/operations/${operationId}${params}`);
},
async cancel(operationId: string, instanceName: string): Promise<{ message: string }> {
return apiClient.post(`/api/v1/operations/${operationId}/cancel?instance=${instanceName}`);
},
// SSE stream for operation updates
createStream(operationId: string): EventSource {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055';
return new EventSource(`${baseUrl}/api/v1/operations/${operationId}/stream`);
},
};

29
src/services/api/pxe.ts Normal file
View File

@@ -0,0 +1,29 @@
import { apiClient } from './client';
import type {
PxeAssetsResponse,
PxeAsset,
DownloadAssetRequest,
OperationResponse,
PxeAssetType,
} from './types';
export const pxeApi = {
async listAssets(instanceName: string): Promise<PxeAssetsResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/pxe/assets`);
},
async getAsset(instanceName: string, type: PxeAssetType): Promise<PxeAsset> {
return apiClient.get(`/api/v1/instances/${instanceName}/pxe/assets/${type}`);
},
async downloadAsset(
instanceName: string,
request: DownloadAssetRequest
): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/pxe/assets/download`, request);
},
async deleteAsset(instanceName: string, type: PxeAssetType): Promise<{ message: string }> {
return apiClient.delete(`/api/v1/instances/${instanceName}/pxe/assets/${type}`);
},
};

View File

@@ -0,0 +1,62 @@
import { apiClient } from './client';
import type {
ServiceListResponse,
Service,
ServiceStatus,
ServiceManifest,
ServiceInstallRequest,
OperationResponse,
} from './types';
export const servicesApi = {
// Instance services
async list(instanceName: string): Promise<ServiceListResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/services`);
},
async get(instanceName: string, serviceName: string): Promise<Service> {
return apiClient.get(`/api/v1/instances/${instanceName}/services/${serviceName}`);
},
async install(instanceName: string, service: ServiceInstallRequest): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/services`, service);
},
async installAll(instanceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/services/install-all`);
},
async delete(instanceName: string, serviceName: string): Promise<OperationResponse> {
return apiClient.delete(`/api/v1/instances/${instanceName}/services/${serviceName}`);
},
async getStatus(instanceName: string, serviceName: string): Promise<ServiceStatus> {
return apiClient.get(`/api/v1/instances/${instanceName}/services/${serviceName}/status`);
},
async getConfig(instanceName: string, serviceName: string): Promise<Record<string, unknown>> {
return apiClient.get(`/api/v1/instances/${instanceName}/services/${serviceName}/config`);
},
// Service lifecycle
async fetch(instanceName: string, serviceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/services/${serviceName}/fetch`);
},
async compile(instanceName: string, serviceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/services/${serviceName}/compile`);
},
async deploy(instanceName: string, serviceName: string): Promise<OperationResponse> {
return apiClient.post(`/api/v1/instances/${instanceName}/services/${serviceName}/deploy`);
},
// Global service info (not instance-specific)
async getManifest(serviceName: string): Promise<ServiceManifest> {
return apiClient.get(`/api/v1/services/${serviceName}/manifest`);
},
async getGlobalConfig(serviceName: string): Promise<Record<string, unknown>> {
return apiClient.get(`/api/v1/services/${serviceName}/config`);
},
};

View File

@@ -0,0 +1,53 @@
export interface App {
name: string;
description: string;
version: string;
category?: string;
icon?: string;
requires?: AppRequirement[];
defaultConfig?: Record<string, unknown>;
requiredSecrets?: string[];
dependencies?: string[];
config?: Record<string, string>;
status?: AppStatus;
}
export interface AppRequirement {
name: string;
}
export interface DeployedApp {
name: string;
status: 'added' | 'deployed';
version?: string;
namespace?: string;
url?: string;
}
export interface AppStatus {
status: 'available' | 'added' | 'deploying' | 'deployed' | 'running' | 'error' | 'stopped';
message?: string;
namespace?: string;
replicas?: number;
resources?: AppResources;
}
export interface AppResources {
cpu?: string;
memory?: string;
storage?: string;
}
export interface AppListResponse {
apps: App[];
}
export interface AppAddRequest {
name: string;
config?: Record<string, string>;
}
export interface AppAddResponse {
message: string;
app: string;
}

View File

@@ -0,0 +1,45 @@
export interface ClusterConfig {
clusterName: string;
vip: string;
version?: string;
}
export interface ClusterStatus {
ready: boolean;
nodes: number;
controlPlaneNodes: number;
workerNodes: number;
kubernetesVersion?: string;
talosVersion?: string;
}
export interface HealthCheck {
name: string;
status: 'passing' | 'warning' | 'failing';
message: string;
}
export interface ClusterHealthResponse {
status: 'healthy' | 'degraded' | 'unhealthy';
checks: HealthCheck[];
}
export interface KubeconfigResponse {
kubeconfig: string;
}
export interface TalosconfigResponse {
talosconfig: string;
}
export interface ClusterBootstrapRequest {
node: string;
}
export interface ClusterEndpointsRequest {
include_nodes?: boolean;
}
export interface ClusterResetRequest {
confirm: boolean;
}

View File

@@ -0,0 +1,17 @@
export interface ConfigUpdate {
path: string;
value: unknown;
}
export interface ConfigUpdateBatchRequest {
updates: ConfigUpdate[];
}
export interface ConfigUpdateResponse {
message: string;
updated?: number;
}
export interface SecretsResponse {
[key: string]: string;
}

View File

@@ -0,0 +1,12 @@
export interface ContextResponse {
context: string | null;
}
export interface SetContextRequest {
context: string;
}
export interface SetContextResponse {
context: string;
message: string;
}

View File

@@ -0,0 +1,9 @@
export * from './instance';
export * from './context';
export * from './operation';
export * from './config';
export * from './node';
export * from './cluster';
export * from './app';
export * from './service';
export * from './pxe';

View File

@@ -0,0 +1,27 @@
export interface Instance {
name: string;
config: Record<string, unknown>;
}
export interface InstanceListResponse {
instances: string[];
}
export interface CreateInstanceRequest {
name: string;
}
export interface CreateInstanceResponse {
name: string;
message: string;
warning?: string;
}
export interface DeleteInstanceResponse {
message: string;
}
export interface GetInstanceResponse {
name: string;
config: Record<string, unknown>;
}

View File

@@ -0,0 +1,58 @@
export interface Node {
hostname: string;
target_ip: string;
role: 'controlplane' | 'worker';
current_ip?: string;
interface?: string;
disk?: string;
version?: string;
schematic_id?: string;
// Backend state flags for deriving status
maintenance?: boolean;
configured?: boolean;
applied?: boolean;
// Optional fields (not yet returned by API)
hardware?: HardwareInfo;
talosVersion?: string;
kubernetesVersion?: string;
}
export interface HardwareInfo {
cpu?: string;
memory?: string;
disk?: string;
manufacturer?: string;
model?: string;
}
export interface DiscoveredNode {
ip: string;
hostname?: string;
maintenance_mode?: boolean;
version?: string;
interface?: string;
disks?: string[];
}
export interface DiscoveryStatus {
active: boolean;
started_at?: string;
nodes_found?: DiscoveredNode[];
error?: string;
}
export interface NodeListResponse {
nodes: Node[];
}
export interface NodeAddRequest {
hostname: string;
target_ip: string;
role: 'controlplane' | 'worker';
disk?: string;
}
export interface NodeUpdateRequest {
role?: 'controlplane' | 'worker';
config?: Record<string, unknown>;
}

View File

@@ -0,0 +1,21 @@
export interface Operation {
id: string;
instance_name: string;
type: string;
target: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
message: string;
progress: number;
started: string;
completed?: string;
error?: string;
}
export interface OperationListResponse {
operations: Operation[];
}
export interface OperationResponse {
operation_id: string;
message: string;
}

View File

@@ -0,0 +1,27 @@
export type PxeAssetType = 'kernel' | 'initramfs' | 'iso';
export type PxeAssetStatus = 'available' | 'missing' | 'downloading' | 'error';
export interface PxeAsset {
type: PxeAssetType;
status: PxeAssetStatus;
version?: string;
size?: string;
path?: string;
error?: string;
}
export interface PxeAssetsResponse {
assets: PxeAsset[];
}
export interface DownloadAssetRequest {
type: PxeAssetType;
version?: string;
url: string;
}
export interface OperationResponse {
operation_id: string;
message: string;
}

View File

@@ -0,0 +1,29 @@
export interface Service {
name: string;
description: string;
version?: string;
status?: ServiceStatus;
deployed?: boolean;
}
export interface ServiceStatus {
status: 'available' | 'deploying' | 'running' | 'error' | 'stopped';
message?: string;
namespace?: string;
ready?: boolean;
}
export interface ServiceListResponse {
services: Service[];
}
export interface ServiceManifest {
name: string;
version: string;
description: string;
config: Record<string, unknown>;
}
export interface ServiceInstallRequest {
name: string;
}

View File

@@ -0,0 +1,41 @@
import { apiClient } from './client';
export interface HealthResponse {
status: string;
[key: string]: unknown;
}
export interface VersionResponse {
version: string;
[key: string]: unknown;
}
export const utilitiesApi = {
async health(): Promise<HealthResponse> {
return apiClient.get('/api/v1/utilities/health');
},
async instanceHealth(instanceName: string): Promise<HealthResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/utilities/health`);
},
async getDashboardToken(): Promise<{ token: string }> {
return apiClient.get('/api/v1/utilities/dashboard/token');
},
async getNodeIPs(): Promise<{ ips: string[] }> {
return apiClient.get('/api/v1/utilities/nodes/ips');
},
async getControlPlaneIP(): Promise<{ ip: string }> {
return apiClient.get('/api/v1/utilities/controlplane/ip');
},
async copySecret(secret: string, targetInstance: string): Promise<{ message: string }> {
return apiClient.post(`/api/v1/utilities/secrets/${secret}/copy`, { target: targetInstance });
},
async getVersion(): Promise<VersionResponse> {
return apiClient.get('/api/v1/utilities/version');
},
};