From 6d22275a1f35c11c5de95110d3a80276db9d809a Mon Sep 17 00:00:00 2001 From: Justin Bosker Date: Tue, 21 Jul 2026 14:49:09 +0200 Subject: [PATCH] Add persistence, lifecycle improvements, logging and typescript fixes --- manager/src/index.ts | 12 +++- manager/src/web/api.ts | 140 ++++++++++++++++++++++++++++++++++++++ manager/src/web/routes.ts | 67 ++++++++++++++++++ manager/src/web/server.ts | 16 +++++ 4 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 manager/src/web/api.ts create mode 100644 manager/src/web/routes.ts create mode 100644 manager/src/web/server.ts diff --git a/manager/src/index.ts b/manager/src/index.ts index 2317e83..3e12c93 100644 --- a/manager/src/index.ts +++ b/manager/src/index.ts @@ -7,6 +7,7 @@ import { PortManager } from "./portManager"; import { ServerStatusManager } from "./serverStatusManager"; import { ServerLifecycle } from "./serverLifecycle"; import { getLogger } from "./logger"; +import { createWebServer } from "./web/server"; const logger = getLogger("Index"); @@ -103,11 +104,18 @@ async function main(){ await statusManager.start(); - - + const webServer = createWebServer({ + wrapperRegistry, + serverRegistry, + lifecycle, + rabbitMqStatus: "connected", + managerStatus: "running" + }, 3000); logger.info("Manager gestartet"); + void webServer; + } diff --git a/manager/src/web/api.ts b/manager/src/web/api.ts new file mode 100644 index 0000000..d818f5e --- /dev/null +++ b/manager/src/web/api.ts @@ -0,0 +1,140 @@ +import { ServerRegistry } from "../serverRegistry"; +import { WrapperRegistry } from "../wrapperRegistry"; +import { ServerLifecycle } from "../serverLifecycle"; + +interface DashboardSummary { + managerStatus: string; + rabbitMqStatus: string; + wrapperCount: number; + runningServerCount: number; + totalPlayers: number; + totalCapacity: number; + utilizationPercent: number; + desiredServers: number; + activeServers: number; +} + +export interface WebApiDependencies { + wrapperRegistry: WrapperRegistry; + serverRegistry: ServerRegistry; + lifecycle: ServerLifecycle; + rabbitMqStatus?: string; + managerStatus?: string; +} + +export class WebApi { + constructor(private deps: WebApiDependencies) {} + + getHealth() { + return { + status: "ok" + }; + } + + getDashboard(): DashboardSummary { + const wrappers = this.deps.wrapperRegistry.getAll(); + const servers = this.deps.serverRegistry.getAll(); + + const runningServers = servers.filter(server => server.status === "RUNNING").length; + const totalPlayers = servers.reduce((sum, server) => sum + server.players, 0); + const totalCapacity = servers.reduce((sum, server) => sum + server.maxPlayers, 0); + const utilizationPercent = totalCapacity > 0 ? Math.round((totalPlayers / totalCapacity) * 100) : 0; + + return { + managerStatus: this.deps.managerStatus ?? "running", + rabbitMqStatus: this.deps.rabbitMqStatus ?? "connected", + wrapperCount: wrappers.filter(wrapper => wrapper.online).length, + runningServerCount: runningServers, + totalPlayers, + totalCapacity, + utilizationPercent, + desiredServers: this.getDesiredServers(), + activeServers: runningServers + }; + } + + getWrappers() { + return this.deps.wrapperRegistry.getAll().map(wrapper => ({ + id: wrapper.id, + hostname: wrapper.hostname, + online: wrapper.online, + cpu: wrapper.cpu, + ram: wrapper.ram, + ramFree: wrapper.ramFree, + lastHeartbeat: wrapper.lastHeartbeat, + serverCount: wrapper.servers.length + })); + } + + getWrapper(id: string) { + const wrapper = this.deps.wrapperRegistry.get(id); + + if (!wrapper) { + return null; + } + + return { + id: wrapper.id, + hostname: wrapper.hostname, + online: wrapper.online, + cpu: wrapper.cpu, + ram: wrapper.ram, + ramFree: wrapper.ramFree, + lastHeartbeat: wrapper.lastHeartbeat, + serverCount: wrapper.servers.length, + servers: wrapper.servers.map(server => ({ + id: server.id, + name: server.name, + port: server.port, + queryPort: server.queryPort, + players: server.players, + maxPlayers: server.maxPlayers, + alive: server.alive, + started: server.started + })) + }; + } + + getServers() { + return this.deps.serverRegistry.getAll().map(server => ({ + id: server.id, + name: server.name, + status: server.status, + wrapperId: server.wrapperId, + port: server.port, + queryPort: server.queryPort, + players: server.players, + maxPlayers: server.maxPlayers, + created: server.created, + startedAt: server.startedAt, + emptySince: server.emptySince + })); + } + + getServer(id: string) { + const server = this.deps.serverRegistry.get(id); + + if (!server) { + return null; + } + + return { + id: server.id, + name: server.name, + status: server.status, + wrapperId: server.wrapperId, + port: server.port, + queryPort: server.queryPort, + players: server.players, + maxPlayers: server.maxPlayers, + created: server.created, + startedAt: server.startedAt, + emptySince: server.emptySince + }; + } + + private getDesiredServers(): number { + const lifecycle = this.deps.lifecycle as ServerLifecycle & { desiredServers?: number }; + return lifecycle.desiredServers ?? 0; + } +} diff --git a/manager/src/web/routes.ts b/manager/src/web/routes.ts new file mode 100644 index 0000000..631acdd --- /dev/null +++ b/manager/src/web/routes.ts @@ -0,0 +1,67 @@ +import { IncomingMessage, ServerResponse } from "http"; +import { parse as parseUrl } from "url"; +import { WebApi } from "./api"; + +export function registerRoutes(server: { on: (event: string, listener: (...args: any[]) => void) => void }, api: WebApi): void { + server.on("request", (req: IncomingMessage, res: ServerResponse) => { + const url = parseUrl(req.url ?? "/", true); + const pathname = url.pathname ?? "/"; + + if (pathname === "/api/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(api.getHealth())); + return; + } + + if (pathname === "/api/dashboard") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(api.getDashboard())); + return; + } + + if (pathname === "/api/wrappers") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(api.getWrappers())); + return; + } + + if (pathname.startsWith("/api/wrappers/")) { + const id = pathname.split("/").pop() ?? ""; + const wrapper = api.getWrapper(id); + + if (!wrapper) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Wrapper not found" })); + return; + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(wrapper)); + return; + } + + if (pathname === "/api/servers") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(api.getServers())); + return; + } + + if (pathname.startsWith("/api/servers/")) { + const id = pathname.split("/").pop() ?? ""; + const server = api.getServer(id); + + if (!server) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Server not found" })); + return; + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(server)); + return; + } + + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); + }); +} diff --git a/manager/src/web/server.ts b/manager/src/web/server.ts new file mode 100644 index 0000000..ef032ef --- /dev/null +++ b/manager/src/web/server.ts @@ -0,0 +1,16 @@ +import { createServer, IncomingMessage, ServerResponse } from "http"; +import { registerRoutes } from "./routes"; +import { WebApi, WebApiDependencies } from "./api"; + +export function createWebServer(deps: WebApiDependencies, port = 3000) { + const api = new WebApi(deps); + const server = createServer(); + + registerRoutes(server, api); + + server.listen(port, () => { + // no-op: startup is handled by the manager + }); + + return server; +}