fix: preserve server startedAt from wrapper status

This commit is contained in:
2026-07-21 19:29:06 +02:00
parent 6d22275a1f
commit 984992541c
39 changed files with 2299 additions and 97 deletions
+9
View File
@@ -9,6 +9,7 @@ const portManager_1 = require("./portManager");
const serverStatusManager_1 = require("./serverStatusManager");
const serverLifecycle_1 = require("./serverLifecycle");
const logger_1 = require("./logger");
const server_1 = require("./web/server");
const logger = (0, logger_1.getLogger)("Index");
const wrapperRegistry = new wrapperRegistry_1.WrapperRegistry();
const serverRegistry = new serverRegistry_1.ServerRegistry();
@@ -32,6 +33,14 @@ async function main() {
logger.debug("ServerLifecycle gestartet");
const statusManager = new serverStatusManager_1.ServerStatusManager(rabbit, serverRegistry);
await statusManager.start();
const webServer = (0, server_1.createWebServer)({
wrapperRegistry,
serverRegistry,
lifecycle,
rabbitMqStatus: "connected",
managerStatus: "running"
}, 3000);
logger.info("Manager gestartet");
void webServer;
}
main();
+7
View File
@@ -25,6 +25,13 @@ class ServerLifecycle {
this.desiredServers =
count;
}
/**
* Exposes the configured target for monitoring consumers.
* It deliberately does not influence lifecycle behaviour.
*/
getDesiredServers() {
return this.desiredServers;
}
checkServers() {
const servers = this.serverRegistry.getAll();
/*
+12 -1
View File
@@ -85,6 +85,17 @@ export class ServerLifecycle {
}
/**
* Exposes the configured target for monitoring consumers.
* It deliberately does not influence lifecycle behaviour.
*/
getDesiredServers(): number {
return this.desiredServers;
}
@@ -703,4 +714,4 @@ export class ServerLifecycle {
}
}
+4
View File
@@ -118,6 +118,10 @@ class ServerRegistry {
Date.now();
const oldStatus = server.status;
if (srv.alive) {
if (!server.startedAt) {
server.startedAt =
srv.started;
}
server.status =
"RUNNING";
}
+8 -1
View File
@@ -412,6 +412,13 @@ export class ServerRegistry {
if(srv.alive){
if(!server.startedAt){
server.startedAt =
srv.started;
}
server.status =
"RUNNING";
@@ -452,4 +459,4 @@ export class ServerRegistry {
}
}
}
+4
View File
@@ -35,6 +35,10 @@ class ServerStatusManager {
return;
server.status =
"RUNNING";
if (!server.startedAt) {
server.startedAt =
Date.now();
}
this.registry.update(server);
logger.info("Server started:", data.serverId);
}
+8
View File
@@ -161,6 +161,14 @@ export class ServerStatusManager {
"RUNNING";
if(!server.startedAt){
server.startedAt =
Date.now();
}
this.registry.update(
+111
View File
@@ -0,0 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebApi = void 0;
class WebApi {
constructor(deps) {
this.deps = deps;
}
getHealth() {
return {
status: this.deps.managerStatus ?? "running",
timestamp: new Date().toISOString()
};
}
getDashboard() {
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.length,
onlineWrapperCount: wrappers.filter(wrapper => wrapper.online).length,
serverCount: servers.length,
runningServerCount: runningServers,
totalPlayers,
totalCapacity,
utilizationPercent,
desiredServers: this.getDesiredServers(),
activeServers: servers.filter(server => server.status === "RUNNING" || server.status === "STARTING").length
};
}
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) {
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) {
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
};
}
getDesiredServers() {
return this.deps.lifecycle.getDesiredServers();
}
}
exports.WebApi = WebApi;
+22 -22
View File
@@ -1,18 +1,14 @@
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;
}
import {
DashboardSummary,
HealthResponse,
ServerDetail,
ServerSummary,
WrapperDetail,
WrapperSummary
} from "./types";
export interface WebApiDependencies {
wrapperRegistry: WrapperRegistry;
@@ -25,9 +21,10 @@ export interface WebApiDependencies {
export class WebApi {
constructor(private deps: WebApiDependencies) {}
getHealth() {
getHealth(): HealthResponse {
return {
status: "ok"
status: this.deps.managerStatus ?? "running",
timestamp: new Date().toISOString()
};
}
@@ -43,17 +40,21 @@ export class WebApi {
return {
managerStatus: this.deps.managerStatus ?? "running",
rabbitMqStatus: this.deps.rabbitMqStatus ?? "connected",
wrapperCount: wrappers.filter(wrapper => wrapper.online).length,
wrapperCount: wrappers.length,
onlineWrapperCount: wrappers.filter(wrapper => wrapper.online).length,
serverCount: servers.length,
runningServerCount: runningServers,
totalPlayers,
totalCapacity,
utilizationPercent,
desiredServers: this.getDesiredServers(),
activeServers: runningServers
activeServers: servers.filter(server =>
server.status === "RUNNING" || server.status === "STARTING"
).length
};
}
getWrappers() {
getWrappers(): WrapperSummary[] {
return this.deps.wrapperRegistry.getAll().map(wrapper => ({
id: wrapper.id,
hostname: wrapper.hostname,
@@ -66,7 +67,7 @@ export class WebApi {
}));
}
getWrapper(id: string) {
getWrapper(id: string): WrapperDetail | null {
const wrapper = this.deps.wrapperRegistry.get(id);
if (!wrapper) {
@@ -95,7 +96,7 @@ export class WebApi {
};
}
getServers() {
getServers(): ServerSummary[] {
return this.deps.serverRegistry.getAll().map(server => ({
id: server.id,
name: server.name,
@@ -111,7 +112,7 @@ export class WebApi {
}));
}
getServer(id: string) {
getServer(id: string): ServerDetail | null {
const server = this.deps.serverRegistry.get(id);
if (!server) {
@@ -134,7 +135,6 @@ export class WebApi {
}
private getDesiredServers(): number {
const lifecycle = this.deps.lifecycle as ServerLifecycle & { desiredServers?: number };
return lifecycle.desiredServers ?? 0;
return this.deps.lifecycle.getDesiredServers();
}
}
+56
View File
@@ -0,0 +1,56 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerRoutes = registerRoutes;
const url_1 = require("url");
function registerRoutes(server, api) {
server.on("request", (req, res) => {
const url = (0, url_1.parse)(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 = decodeURIComponent(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 = decodeURIComponent(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" }));
});
}
+2 -2
View File
@@ -26,7 +26,7 @@ export function registerRoutes(server: { on: (event: string, listener: (...args:
}
if (pathname.startsWith("/api/wrappers/")) {
const id = pathname.split("/").pop() ?? "";
const id = decodeURIComponent(pathname.split("/").pop() ?? "");
const wrapper = api.getWrapper(id);
if (!wrapper) {
@@ -47,7 +47,7 @@ export function registerRoutes(server: { on: (event: string, listener: (...args:
}
if (pathname.startsWith("/api/servers/")) {
const id = pathname.split("/").pop() ?? "";
const id = decodeURIComponent(pathname.split("/").pop() ?? "");
const server = api.getServer(id);
if (!server) {
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWebServer = createWebServer;
const http_1 = require("http");
const routes_1 = require("./routes");
const api_1 = require("./api");
function createWebServer(deps, port = 3000) {
const api = new api_1.WebApi(deps);
const server = (0, http_1.createServer)();
(0, routes_1.registerRoutes)(server, api);
server.listen(port, () => {
// no-op: startup is handled by the manager
});
return server;
}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+58
View File
@@ -0,0 +1,58 @@
export interface HealthResponse {
status: string;
timestamp: string;
}
export interface DashboardSummary {
managerStatus: string;
rabbitMqStatus: string;
wrapperCount: number;
onlineWrapperCount: number;
serverCount: number;
runningServerCount: number;
totalPlayers: number;
totalCapacity: number;
utilizationPercent: number;
desiredServers: number;
activeServers: number;
}
export interface WrapperSummary {
id: string;
hostname: string;
online: boolean;
cpu: number;
ram: number;
ramFree: number;
lastHeartbeat: number;
serverCount: number;
}
export interface WrapperDetail extends WrapperSummary {
servers: Array<{
id: string;
name: string;
port: number;
queryPort: number;
players: number;
maxPlayers: number;
alive: boolean;
started: number;
}>;
}
export interface ServerSummary {
id: string;
name: string;
status: string;
wrapperId: string;
port: number;
queryPort: number;
players: number;
maxPlayers: number;
created: number;
startedAt?: number;
emptySince?: number;
}
export type ServerDetail = ServerSummary;