luneraria-control-center/server.js
Martin Stickelbroeck 0df07eefd1
Some checks are pending
CI/CD Cloud Deployment / test-and-build (push) Waiting to run
feat: Champions of Luneraria - Portal & LiveOps Schaltzentrale
- Triple-A Werbeseite mit Gameplay-Videos, 3-Lane Radar und Einheiten-Showcase
- LiveOps-Schaltzentrale mit CCU-Tracking, Download-Statistiken und Match-Balance
- Reale Engine-Benchmarks (C# vs Godot, U2-U5.5 FPS, GPU-Skinning Entlastung)
- Zero-Dependency Node.js Backend mit REST-APIs und Server-Sent Events (SSE)
- Cloud-Ready mit Dockerfile, docker-compose.yml und GitHub Actions CI/CD
2026-10-11 20:07:51 +02:00

222 lines
7.3 KiB
JavaScript

/**
* CHAMPIONS OF LUNERARIA - Portal & Control Center Server
* Production-ready, zero-dependency Node.js HTTP server with REST APIs,
* Server-Sent Events (SSE) for live telemetry, and static asset streaming.
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const telemetry = require('./src/data/telemetry');
const PORT = parseInt(process.env.PORT, 10) || 8080;
const PUBLIC_DIR = path.join(__dirname, 'public');
const MIME_TYPES = {
'.html': 'text/html; charset=UTF-8',
'.css': 'text/css; charset=UTF-8',
'.js': 'application/javascript; charset=UTF-8',
'.json': 'application/json; charset=UTF-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.mp4': 'video/mp4',
'.ttf': 'font/ttf',
'.otf': 'font/otf',
'.woff': 'font/woff',
'.woff2': 'font/woff2'
};
// Connected SSE clients for live metrics push
const sseClients = new Set();
// Broadcast SSE update every 2 seconds
setInterval(() => {
if (sseClients.size === 0) return;
const payload = JSON.stringify(telemetry.getLiveMetrics());
sseClients.forEach(res => {
try {
res.write(`data: ${payload}\n\n`);
} catch (e) {
sseClients.delete(res);
}
});
}, 2000);
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// ------------------------------------------------------------------------
// API Routes
// ------------------------------------------------------------------------
if (pathname === '/api/metrics' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(telemetry.getLiveMetrics()));
return;
}
if (pathname === '/api/benchmarks' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(telemetry.getBenchmarks()));
return;
}
if (pathname === '/api/downloads/history' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(telemetry.getDailyDownloads()));
return;
}
if (pathname === '/api/logs' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(telemetry.getLogs()));
return;
}
// Server-Sent Events (SSE) Live Feed
if (pathname === '/api/events' && req.method === 'GET') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
res.write(`data: ${JSON.stringify(telemetry.getLiveMetrics())}\n\n`);
sseClients.add(res);
req.on('close', () => {
sseClients.delete(res);
});
return;
}
// Operations Control APIs (POST)
if (pathname.startsWith('/api/control/') && req.method === 'POST') {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
let data = {};
try { if (body) data = JSON.parse(body); } catch (e) {}
if (pathname === '/api/control/tickrate') {
const newRate = telemetry.setTickrate(data.rate || 20);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, targetTickrate: newRate }));
return;
}
if (pathname === '/api/control/maintenance') {
const mode = telemetry.toggleMaintenance();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, maintenanceMode: mode }));
return;
}
if (pathname === '/api/control/spike') {
const newBaseline = telemetry.simulateSpike(data.amount || 25);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, ccuBaseline: newBaseline }));
return;
}
if (pathname === '/api/control/broadcast') {
const msg = data.message || 'Server-Ankündigung';
telemetry.addLog('WARN', 'BROADCAST', msg);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, message: msg }));
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unknown control action' }));
});
return;
}
// ------------------------------------------------------------------------
// Static Page Routes
// ------------------------------------------------------------------------
let filePath = '';
if (pathname === '/' || pathname === '/index.html') {
filePath = path.join(PUBLIC_DIR, 'index.html');
} else if (pathname === '/dashboard' || pathname === '/dashboard.html') {
filePath = path.join(PUBLIC_DIR, 'dashboard.html');
} else {
filePath = path.join(PUBLIC_DIR, pathname);
}
// Security: prevent directory traversal
if (!filePath.startsWith(PUBLIC_DIR)) {
res.writeHead(403);
res.end('Access Denied');
return;
}
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=UTF-8' });
res.end('404 Not Found - Champions of Luneraria');
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
// Support video streaming (HTTP 206 Partial Content)
if (ext === '.mp4') {
const range = req.headers.range;
const fileSize = stats.size;
if (range) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = (end - start) + 1;
const fileStream = fs.createReadStream(filePath, { start, end });
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4'
});
fileStream.pipe(res);
return;
}
}
// Standard static file response
res.writeHead(200, {
'Content-Type': contentType,
'Content-Length': stats.size,
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=86400'
});
fs.createReadStream(filePath).pipe(res);
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log('===============================================================');
console.log(` ⚔️ CHAMPIONS OF LUNERARIA - PORTAL & CONTROL CENTER READY`);
console.log('===============================================================');
console.log(` 🌐 Öffentliche Website: http://localhost:${PORT}/`);
console.log(` 🎛️ Schaltzentrale / Ops: http://localhost:${PORT}/dashboard`);
console.log(` 📊 Live Telemetrie API: http://localhost:${PORT}/api/metrics`);
console.log(` 📈 Benchmarks API: http://localhost:${PORT}/api/benchmarks`);
console.log('---------------------------------------------------------------');
});