Refactor project structure

This commit is contained in:
2025-07-26 11:37:28 +02:00
parent 1fb6b7c212
commit f337a3efe2
48 changed files with 0 additions and 0 deletions

146
webui/src/lib/api.ts Normal file
View File

@@ -0,0 +1,146 @@
import { CreateInstanceOptions, Instance } from "@/types/instance"
const API_BASE = '/api/v1'
// Configuration for API calls
// interface ApiConfig {
// apiKey?: string
// }
// Global config - can be updated when auth is added
// let apiConfig: ApiConfig = {}
// export const setApiConfig = (config: ApiConfig) => {
// apiConfig = config
// }
// Base API call function with error handling
async function apiCall<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${API_BASE}${endpoint}`
// Prepare headers
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
}
// Add API key - not supported yet
// if (apiConfig.apiKey) {
// headers['Authorization'] = `Bearer ${apiConfig.apiKey}`
// }
try {
const response = await fetch(url, {
...options,
headers,
})
if (!response.ok) {
// Try to get error message from response
let errorMessage = `HTTP ${response.status}`
try {
const errorText = await response.text()
if (errorText) {
errorMessage += `: ${errorText}`
}
} catch {
// If we can't read the error, just use status
}
throw new Error(errorMessage)
}
// Handle empty responses (like DELETE)
if (response.status === 204) {
return undefined as T
}
const data = await response.json()
return data
} catch (error) {
if (error instanceof Error) {
throw error
}
throw new Error('Network error occurred')
}
}
// Server API functions
export const serverApi = {
// GET /server/help
getHelp: async (): Promise<string> => {
const response = await fetch(`${API_BASE}/server/help`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
// GET /server/version
getVersion: async (): Promise<string> => {
const response = await fetch(`${API_BASE}/server/version`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
// GET /server/devices
getDevices: async (): Promise<string> => {
const response = await fetch(`${API_BASE}/server/devices`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
}
// Instance API functions
export const instancesApi = {
// GET /instances
list: () => apiCall<Instance[]>('/instances'),
// GET /instances/{name}
get: (name: string) => apiCall<Instance>(`/instances/${name}`),
// POST /instances/{name}
create: (name: string, options: CreateInstanceOptions) =>
apiCall<Instance>(`/instances/${name}`, {
method: 'POST',
body: JSON.stringify(options),
}),
// PUT /instances/{name}
update: (name: string, options: CreateInstanceOptions) =>
apiCall<Instance>(`/instances/${name}`, {
method: 'PUT',
body: JSON.stringify(options),
}),
// DELETE /instances/{name}
delete: (name: string) =>
apiCall<void>(`/instances/${name}`, {
method: 'DELETE',
}),
// POST /instances/{name}/start
start: (name: string) =>
apiCall<Instance>(`/instances/${name}/start`, {
method: 'POST',
}),
// POST /instances/{name}/stop
stop: (name: string) =>
apiCall<Instance>(`/instances/${name}/stop`, {
method: 'POST',
}),
// POST /instances/{name}/restart
restart: (name: string) =>
apiCall<Instance>(`/instances/${name}/restart`, {
method: 'POST',
}),
// GET /instances/{name}/logs
getLogs: (name: string, lines?: number) => {
const params = lines ? `?lines=${lines}` : ''
return apiCall<string>(`/instances/${name}/logs${params}`)
},
}

View File

@@ -0,0 +1,115 @@
import { HealthStatus } from '@/types/instance'
type HealthCallback = (health: HealthStatus) => void
class HealthService {
private intervals: Map<string, NodeJS.Timeout> = new Map()
private callbacks: Map<string, Set<HealthCallback>> = new Map()
async checkHealth(instanceName: string): Promise<HealthStatus> {
try {
const response = await fetch(`/api/v1/instances/${instanceName}/proxy/health`)
if (response.status === 200) {
return {
status: 'ok',
lastChecked: new Date()
}
} else if (response.status === 503) {
const data = await response.json()
return {
status: 'loading',
message: data.error.message,
lastChecked: new Date()
}
} else {
return {
status: 'error',
message: `HTTP ${response.status}`,
lastChecked: new Date()
}
}
} catch (error) {
return {
status: 'error',
message: 'Network error',
lastChecked: new Date()
}
}
}
subscribe(instanceName: string, callback: HealthCallback): () => void {
if (!this.callbacks.has(instanceName)) {
this.callbacks.set(instanceName, new Set())
}
this.callbacks.get(instanceName)!.add(callback)
// Start health checking if this is the first subscriber
if (this.callbacks.get(instanceName)!.size === 1) {
this.startHealthCheck(instanceName)
}
// Return unsubscribe function
return () => {
const callbacks = this.callbacks.get(instanceName)
if (callbacks) {
callbacks.delete(callback)
// Stop health checking if no more subscribers
if (callbacks.size === 0) {
this.stopHealthCheck(instanceName)
this.callbacks.delete(instanceName)
}
}
}
}
private startHealthCheck(instanceName: string): void {
if (this.intervals.has(instanceName)) {
return // Already checking
}
// Initial check with delay
setTimeout(async () => {
const health = await this.checkHealth(instanceName)
this.notifyCallbacks(instanceName, health)
// Start periodic checks
const interval = setInterval(async () => {
const health = await this.checkHealth(instanceName)
this.notifyCallbacks(instanceName, health)
}, 60000)
this.intervals.set(instanceName, interval)
}, 2000)
}
private stopHealthCheck(instanceName: string): void {
const interval = this.intervals.get(instanceName)
if (interval) {
clearInterval(interval)
this.intervals.delete(instanceName)
}
}
private notifyCallbacks(instanceName: string, health: HealthStatus): void {
const callbacks = this.callbacks.get(instanceName)
if (callbacks) {
callbacks.forEach(callback => callback(health))
}
}
stopAll(): void {
this.intervals.forEach(interval => clearInterval(interval))
this.intervals.clear()
this.callbacks.clear()
}
}
export const healthService = new HealthService()
// Export the individual checkHealth function as well
export async function checkHealth(instanceName: string): Promise<HealthStatus> {
return healthService.checkHealth(instanceName)
}

6
webui/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,59 @@
import { CreateInstanceOptions, getAllFieldKeys } from '@/schemas/instanceOptions'
// Only define the basic fields we want to show by default
export const basicFieldsConfig: Record<string, {
label: string
description?: string
placeholder?: string
required?: boolean
}> = {
auto_restart: {
label: 'Auto Restart',
description: 'Automatically restart the instance on failure'
},
max_restarts: {
label: 'Max Restarts',
placeholder: '3',
description: 'Maximum number of restart attempts (0 = unlimited)'
},
restart_delay: {
label: 'Restart Delay (seconds)',
placeholder: '5',
description: 'Delay in seconds before attempting restart'
},
model: {
label: 'Model Path',
placeholder: '/path/to/model.gguf',
description: 'Path to the model file'
},
hf_repo: {
label: 'Hugging Face Repository',
placeholder: 'microsoft/DialoGPT-medium',
description: 'Hugging Face model repository'
},
hf_file: {
label: 'Hugging Face File',
placeholder: 'model.gguf',
description: 'Specific file in the repository'
},
gpu_layers: {
label: 'GPU Layers',
placeholder: '0',
description: 'Number of layers to offload to GPU'
}
}
export function isBasicField(key: keyof CreateInstanceOptions): boolean {
return key in basicFieldsConfig
}
export function getBasicFields(): (keyof CreateInstanceOptions)[] {
return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[]
}
export function getAdvancedFields(): (keyof CreateInstanceOptions)[] {
return getAllFieldKeys().filter(key => !isBasicField(key))
}
// Re-export the Zod-based functions
export { getFieldType } from '@/schemas/instanceOptions'