This commit is contained in:
2025-11-18 14:16:27 +00:00
parent 1fe3cd7f14
commit 44267bbb27
9 changed files with 703 additions and 160 deletions

View File

@@ -13,6 +13,7 @@ export class OneboxHttpServer {
private oneboxRef: Onebox;
private server: Deno.HttpServer | null = null;
private port = 3000;
private wsClients: Set<WebSocket> = new Set();
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
@@ -70,6 +71,11 @@ export class OneboxHttpServer {
logger.debug(`${req.method} ${path}`);
try {
// WebSocket upgrade
if (path === '/api/ws' && req.headers.get('upgrade') === 'websocket') {
return this.handleWebSocketUpgrade(req);
}
// API routes
if (path.startsWith('/api/')) {
return await this.handleApiRequest(req, path);
@@ -184,7 +190,7 @@ export class OneboxHttpServer {
return await this.handleStatusRequest();
} else if (path === '/api/settings' && method === 'GET') {
return await this.handleGetSettingsRequest();
} else if (path === '/api/settings' && method === 'PUT') {
} else if (path === '/api/settings' && (method === 'PUT' || method === 'POST')) {
return await this.handleUpdateSettingsRequest(req);
} else if (path === '/api/services' && method === 'GET') {
return await this.handleListServicesRequest();
@@ -268,52 +274,117 @@ export class OneboxHttpServer {
}
private async handleStatusRequest(): Promise<Response> {
const status = await this.oneboxRef.getSystemStatus();
return this.jsonResponse({ success: true, data: status });
try {
const status = await this.oneboxRef.getSystemStatus();
return this.jsonResponse({ success: true, data: status });
} catch (error) {
logger.error(`Failed to get system status: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to get system status' }, 500);
}
}
private async handleListServicesRequest(): Promise<Response> {
const services = this.oneboxRef.services.listServices();
return this.jsonResponse({ success: true, data: services });
try {
const services = this.oneboxRef.services.listServices();
return this.jsonResponse({ success: true, data: services });
} catch (error) {
logger.error(`Failed to list services: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to list services' }, 500);
}
}
private async handleDeployServiceRequest(req: Request): Promise<Response> {
const body = await req.json();
const service = await this.oneboxRef.services.deployService(body);
return this.jsonResponse({ success: true, data: service });
try {
const body = await req.json();
const service = await this.oneboxRef.services.deployService(body);
// Broadcast service created
this.broadcastServiceUpdate(service.name, 'created', service);
return this.jsonResponse({ success: true, data: service });
} catch (error) {
logger.error(`Failed to deploy service: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to deploy service' }, 500);
}
}
private async handleGetServiceRequest(name: string): Promise<Response> {
const service = this.oneboxRef.services.getService(name);
if (!service) {
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
try {
const service = this.oneboxRef.services.getService(name);
if (!service) {
return this.jsonResponse({ success: false, error: 'Service not found' }, 404);
}
return this.jsonResponse({ success: true, data: service });
} catch (error) {
logger.error(`Failed to get service ${name}: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to get service' }, 500);
}
return this.jsonResponse({ success: true, data: service });
}
private async handleDeleteServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.removeService(name);
return this.jsonResponse({ success: true, message: 'Service removed' });
try {
await this.oneboxRef.services.removeService(name);
// Broadcast service deleted
this.broadcastServiceUpdate(name, 'deleted');
return this.jsonResponse({ success: true, message: 'Service removed' });
} catch (error) {
logger.error(`Failed to delete service ${name}: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to delete service' }, 500);
}
}
private async handleStartServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.startService(name);
return this.jsonResponse({ success: true, message: 'Service started' });
try {
await this.oneboxRef.services.startService(name);
// Broadcast service started
this.broadcastServiceUpdate(name, 'started');
return this.jsonResponse({ success: true, message: 'Service started' });
} catch (error) {
logger.error(`Failed to start service ${name}: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to start service' }, 500);
}
}
private async handleStopServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.stopService(name);
return this.jsonResponse({ success: true, message: 'Service stopped' });
try {
await this.oneboxRef.services.stopService(name);
// Broadcast service stopped
this.broadcastServiceUpdate(name, 'stopped');
return this.jsonResponse({ success: true, message: 'Service stopped' });
} catch (error) {
logger.error(`Failed to stop service ${name}: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to stop service' }, 500);
}
}
private async handleRestartServiceRequest(name: string): Promise<Response> {
await this.oneboxRef.services.restartService(name);
return this.jsonResponse({ success: true, message: 'Service restarted' });
try {
await this.oneboxRef.services.restartService(name);
// Broadcast service updated
this.broadcastServiceUpdate(name, 'updated');
return this.jsonResponse({ success: true, message: 'Service restarted' });
} catch (error) {
logger.error(`Failed to restart service ${name}: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to restart service' }, 500);
}
}
private async handleGetLogsRequest(name: string): Promise<Response> {
const logs = await this.oneboxRef.services.getServiceLogs(name);
return this.jsonResponse({ success: true, data: logs });
try {
const logs = await this.oneboxRef.services.getServiceLogs(name);
return this.jsonResponse({ success: true, data: logs });
} catch (error) {
logger.error(`Failed to get logs for service ${name}: ${error.message}`);
return this.jsonResponse({ success: false, error: error.message || 'Failed to get logs' }, 500);
}
}
private async handleGetSettingsRequest(): Promise<Response> {
@@ -332,11 +403,32 @@ export class OneboxHttpServer {
);
}
// Update each setting
for (const [key, value] of Object.entries(body)) {
if (typeof value === 'string') {
this.oneboxRef.database.setSetting(key, value);
logger.info(`Setting updated: ${key}`);
// Handle three formats:
// 1. Single setting: { key: "settingName", value: "settingValue" }
// 2. Array format: [{ key: "name1", value: "val1" }, ...]
// 3. Object format: { settingName1: "value1", settingName2: "value2", ... }
if (Array.isArray(body)) {
// Array format from UI
for (const item of body) {
if (item.key && typeof item.value === 'string') {
this.oneboxRef.database.setSetting(item.key, item.value);
logger.info(`Setting updated: ${item.key} = ${item.value}`);
}
}
} else if (body.key && body.value !== undefined) {
// Single setting format: { key: "name", value: "val" }
if (typeof body.value === 'string') {
this.oneboxRef.database.setSetting(body.key, body.value);
logger.info(`Setting updated: ${body.key} = ${body.value}`);
}
} else {
// Object format: { name1: "val1", name2: "val2", ... }
for (const [key, value] of Object.entries(body)) {
if (typeof value === 'string') {
this.oneboxRef.database.setSetting(key, value);
logger.info(`Setting updated: ${key} = ${value}`);
}
}
}
@@ -350,6 +442,102 @@ export class OneboxHttpServer {
}
}
/**
* Handle WebSocket upgrade
*/
private handleWebSocketUpgrade(req: Request): Response {
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = () => {
this.wsClients.add(socket);
logger.info(`WebSocket client connected (${this.wsClients.size} total)`);
// Send initial connection message
socket.send(JSON.stringify({
type: 'connected',
message: 'Connected to Onebox server',
timestamp: Date.now(),
}));
};
socket.onclose = () => {
this.wsClients.delete(socket);
logger.info(`WebSocket client disconnected (${this.wsClients.size} remaining)`);
};
socket.onerror = (error) => {
logger.error(`WebSocket error: ${error}`);
this.wsClients.delete(socket);
};
return response;
}
/**
* Broadcast message to all connected WebSocket clients
*/
broadcast(message: Record<string, any>): void {
const data = JSON.stringify(message);
let successCount = 0;
let failCount = 0;
for (const client of this.wsClients) {
try {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
successCount++;
} else {
this.wsClients.delete(client);
failCount++;
}
} catch (error) {
logger.error(`Failed to send to WebSocket client: ${error.message}`);
this.wsClients.delete(client);
failCount++;
}
}
if (successCount > 0) {
logger.debug(`Broadcast to ${successCount} clients (${failCount} failed)`);
}
}
/**
* Broadcast service update
*/
broadcastServiceUpdate(serviceName: string, action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped', data?: any): void {
this.broadcast({
type: 'service_update',
action,
serviceName,
data,
timestamp: Date.now(),
});
}
/**
* Broadcast service status update
*/
broadcastServiceStatus(serviceName: string, status: string): void {
this.broadcast({
type: 'service_status',
serviceName,
status,
timestamp: Date.now(),
});
}
/**
* Broadcast system status update
*/
broadcastSystemStatus(status: any): void {
this.broadcast({
type: 'system_status',
data: status,
timestamp: Date.now(),
});
}
/**
* Helper to create JSON response
*/