update
This commit is contained in:
325
ecoos_daemon/ts/ui/server.ts
Normal file
325
ecoos_daemon/ts/ui/server.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* UI Server
|
||||
*
|
||||
* HTTP server for the management UI on port 3006
|
||||
*/
|
||||
|
||||
import type { EcoDaemon } from '../daemon/index.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();
|
||||
}
|
||||
|
||||
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 = 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 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
||||
status: 404,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
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</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0a0a;
|
||||
--card: #141414;
|
||||
--border: #2a2a2a;
|
||||
--text: #e0e0e0;
|
||||
--text-dim: #888;
|
||||
--accent: #3b82f6;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
h1 { font-size: 24px; margin-bottom: 20px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.stat { margin-bottom: 8px; }
|
||||
.stat-label { color: var(--text-dim); font-size: 12px; }
|
||||
.stat-value { font-size: 18px; font-weight: 600; }
|
||||
.progress-bar {
|
||||
background: var(--border);
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.logs {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
background: #0d0d0d;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.log-entry { white-space: pre-wrap; word-break: break-all; }
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-dot.running { background: var(--success); }
|
||||
.status-dot.stopped { background: var(--error); }
|
||||
.network-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.network-item:last-child { border-bottom: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>EcoOS Management</h1>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Services</h2>
|
||||
<div class="stat">
|
||||
<span class="status-dot" id="sway-status"></span>
|
||||
Sway Compositor
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="status-dot" id="chrome-status"></span>
|
||||
Chrome Browser
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>CPU</h2>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Model</div>
|
||||
<div class="stat-value" id="cpu-model">-</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Cores</div>
|
||||
<div class="stat-value" id="cpu-cores">-</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Usage</div>
|
||||
<div class="stat-value" id="cpu-usage">-</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="cpu-bar"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Memory</h2>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Used / Total</div>
|
||||
<div class="stat-value" id="memory-usage">-</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="memory-bar"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Network</h2>
|
||||
<div id="network-list"></div>
|
||||
</div>
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>Logs</h2>
|
||||
<div class="logs" id="logs"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function updateStatus(data) {
|
||||
// Services
|
||||
document.getElementById('sway-status').className =
|
||||
'status-dot ' + (data.sway ? 'running' : 'stopped');
|
||||
document.getElementById('chrome-status').className =
|
||||
'status-dot ' + (data.chrome ? 'running' : 'stopped');
|
||||
|
||||
// System info
|
||||
if (data.systemInfo) {
|
||||
const info = data.systemInfo;
|
||||
|
||||
// CPU
|
||||
if (info.cpu) {
|
||||
document.getElementById('cpu-model').textContent = info.cpu.model;
|
||||
document.getElementById('cpu-cores').textContent = info.cpu.cores;
|
||||
document.getElementById('cpu-usage').textContent = info.cpu.usage + '%';
|
||||
document.getElementById('cpu-bar').style.width = info.cpu.usage + '%';
|
||||
}
|
||||
|
||||
// Memory
|
||||
if (info.memory) {
|
||||
document.getElementById('memory-usage').textContent =
|
||||
formatBytes(info.memory.used) + ' / ' + formatBytes(info.memory.total);
|
||||
document.getElementById('memory-bar').style.width = info.memory.usagePercent + '%';
|
||||
}
|
||||
|
||||
// Network
|
||||
if (info.network) {
|
||||
const list = document.getElementById('network-list');
|
||||
list.innerHTML = info.network.map(n =>
|
||||
'<div class="network-item"><span>' + n.name + '</span><span>' + n.ip + '</span></div>'
|
||||
).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Logs
|
||||
if (data.logs) {
|
||||
const logsEl = document.getElementById('logs');
|
||||
logsEl.innerHTML = data.logs.map(l =>
|
||||
'<div class="log-entry">' + l + '</div>'
|
||||
).join('');
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
fetch('/api/status')
|
||||
.then(r => r.json())
|
||||
.then(updateStatus)
|
||||
.catch(console.error);
|
||||
|
||||
// Periodic refresh
|
||||
setInterval(() => {
|
||||
fetch('/api/status')
|
||||
.then(r => r.json())
|
||||
.then(updateStatus)
|
||||
.catch(console.error);
|
||||
}, 3000);
|
||||
|
||||
// WebSocket for live updates
|
||||
const ws = new WebSocket('ws://' + location.host + '/ws');
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
updateStatus(JSON.parse(e.data));
|
||||
} catch {}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
return new Response(html, {
|
||||
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user