Files
eco_os/ecoos_daemon/ts/ui/server.ts

216 lines
5.9 KiB
TypeScript

/**
* UI Server
*
* HTTP server for the management UI on port 3006
*/
import type { EcoDaemon } from '../daemon/index.ts';
import { VERSION } from '../version.ts';
import { files as bundledFiles } from '../daemon/bundledui.ts';
export class UIServer {
private port: number;
private daemon: EcoDaemon;
private clients: Set<WebSocket> = new Set();
constructor(port: number, daemon: EcoDaemon) {
this.port = port;
this.daemon = daemon;
}
async start(): Promise<void> {
Deno.serve({ port: this.port, hostname: '0.0.0.0' }, (req) =>
this.handleRequest(req)
);
console.log(`Management UI running on http://0.0.0.0:${this.port}`);
}
private async handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
const path = url.pathname;
// Handle WebSocket upgrade
if (path === '/ws') {
return this.handleWebSocket(req);
}
// API routes
if (path.startsWith('/api/')) {
return this.handleApi(req, path);
}
// Static files / UI
if (path === '/' || path === '/index.html') {
return this.serveHtml();
}
// Bundled JavaScript
if (path === '/app.js') {
return this.serveAppJs();
}
return new Response('Not Found', { status: 404 });
}
private handleWebSocket(req: Request): Response {
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = () => {
this.clients.add(socket);
console.log('WebSocket client connected');
};
socket.onclose = () => {
this.clients.delete(socket);
console.log('WebSocket client disconnected');
};
socket.onerror = (e) => {
console.error('WebSocket error:', e);
this.clients.delete(socket);
};
return response;
}
broadcast(data: unknown): void {
const message = JSON.stringify(data);
for (const client of this.clients) {
try {
client.send(message);
} catch {
this.clients.delete(client);
}
}
}
private async handleApi(req: Request, path: string): Promise<Response> {
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
};
if (path === '/api/status') {
const status = await this.daemon.getStatus();
return new Response(JSON.stringify(status), { headers });
}
if (path === '/api/logs') {
const logs = this.daemon.getLogs();
return new Response(JSON.stringify({ logs }), { headers });
}
if (path === '/api/reboot' && req.method === 'POST') {
const result = await this.daemon.rebootSystem();
return new Response(JSON.stringify(result), { headers });
}
if (path === '/api/restart-chromium' && req.method === 'POST') {
const result = await this.daemon.restartChromium();
return new Response(JSON.stringify(result), { headers });
}
if (path === '/api/updates') {
const updates = await this.daemon.getUpdateInfo();
return new Response(JSON.stringify(updates), { headers });
}
if (path === '/api/updates/check' && req.method === 'POST') {
await this.daemon.checkForUpdates();
const updates = await this.daemon.getUpdateInfo();
return new Response(JSON.stringify(updates), { headers });
}
if (path === '/api/upgrade' && req.method === 'POST') {
try {
const body = await req.json();
const version = body.version;
if (!version) {
return new Response(JSON.stringify({ success: false, message: 'Version required' }), { headers });
}
const result = await this.daemon.upgradeToVersion(version);
return new Response(JSON.stringify(result), { headers });
} catch (error) {
return new Response(JSON.stringify({ success: false, message: String(error) }), { headers });
}
}
if (path === '/api/displays') {
const displays = await this.daemon.getDisplays();
return new Response(JSON.stringify({ displays }), { headers });
}
// Display control endpoints: /api/displays/{name}/{action}
const displayMatch = path.match(/^\/api\/displays\/([^/]+)\/(enable|disable|primary)$/);
if (displayMatch && req.method === 'POST') {
const name = decodeURIComponent(displayMatch[1]);
const action = displayMatch[2];
let result;
if (action === 'enable') {
result = await this.daemon.setDisplayEnabled(name, true);
} else if (action === 'disable') {
result = await this.daemon.setDisplayEnabled(name, false);
} else if (action === 'primary') {
result = await this.daemon.setKioskDisplay(name);
}
return new Response(JSON.stringify(result), { headers });
}
return new Response(JSON.stringify({ error: 'Not Found' }), {
status: 404,
headers,
});
}
private serveAppJs(): Response {
// Find the bundle.js file in the bundled content
const bundleFile = bundledFiles.find(f => f.path === 'bundle.js');
if (!bundleFile) {
return new Response('Bundle not found', { status: 500 });
}
// Decode base64 content
const jsContent = atob(bundleFile.contentBase64);
return new Response(jsContent, {
headers: {
'Content-Type': 'application/javascript; charset=utf-8',
'Cache-Control': 'no-cache',
},
});
}
private serveHtml(): Response {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EcoOS Management v${VERSION}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
ecoos-app {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<ecoos-app></ecoos-app>
<script type="module" src="/app.js"></script>
</body>
</html>`;
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
}