diff --git a/public/index.html b/public/index.html index e3e87e6..b4d6f75 100644 --- a/public/index.html +++ b/public/index.html @@ -56,6 +56,16 @@

Statut synchronise toutes les 5 minutes.

+
+
+ Capacite limitee +

Un seul serveur de jeux peut etre lance a la fois afin d'eviter de surcharger le serveur.

+
+
+ Arret automatique +

Les serveurs de jeux se ferment automatiquement si aucun joueur n'est present pendant 1 h.

+
+
diff --git a/public/styles/app.css b/public/styles/app.css index 46a5b82..1dc9dfa 100644 --- a/public/styles/app.css +++ b/public/styles/app.css @@ -253,6 +253,36 @@ button { margin-bottom: 1.2rem; } +.info-banner { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; + margin: -0.25rem 0 1rem; +} + +.info-banner > div { + background: linear-gradient(135deg, rgba(123, 92, 255, 0.14), rgba(0, 132, 255, 0.08)); + border: 1px solid rgba(123, 92, 255, 0.28); + border-radius: 7px; + padding: 0.85rem 0.95rem; +} + +.info-banner strong { + display: block; + color: var(--text); + font-family: var(--font-display); + font-size: 0.76rem; + letter-spacing: 0.1em; + margin-bottom: 0.38rem; + text-transform: uppercase; +} + +.info-banner p { + color: var(--muted); + font-size: 0.9rem; + margin: 0; +} + h2, h3 { margin: 0; @@ -704,6 +734,10 @@ code { max-width: none; } + .info-banner { + grid-template-columns: 1fr; + } + .inline-form { width: 100%; } diff --git a/src/routes/servers.js b/src/routes/servers.js index 5c1543d..93a3003 100644 --- a/src/routes/servers.js +++ b/src/routes/servers.js @@ -5,10 +5,17 @@ import { mergeServersWithStatus } from "../services/status.js"; import { getStartProfile, launchServer } from "../services/serverLauncher.js"; const router = express.Router(); +let launchInProgress = false; router.get("/servers", (req, res) => { - const servers = visibleServers(req.group); - return res.json({ servers: servers.map((server) => serializeServerForGroup(server, req.group)) }); + const allServers = serversWithImages(); + const anyServerActive = allServers.some((server) => server.active); + const servers = allServers.filter((server) => canAccessServer(req.group, server.id)); + return res.json({ + servers: servers.map((server) => + serializeServerForGroup(server, req.group, { anyServerActive }) + ), + }); }); router.post("/servers/:serverId/start", async (req, res) => { @@ -23,10 +30,20 @@ router.post("/servers/:serverId/start", async (req, res) => { return res.status(409).json({ error: "Le serveur est deja en ligne" }); } + if (servers.some((entry) => entry.active)) { + return res.status(409).json({ error: "Un serveur est deja en cours" }); + } + + if (launchInProgress) { + return res.status(409).json({ error: "Un lancement est deja en cours" }); + } + if (!getStartProfile(server)) { return res.status(400).json({ error: "Aucun profil de lancement configure" }); } + launchInProgress = true; + try { const { stdout, stderr } = await launchServer(server); @@ -40,24 +57,24 @@ router.post("/servers/:serverId/start", async (req, res) => { return res.json({ status: "started", - server: serializeServerForGroup(server, req.group), + server: serializeServerForGroup(server, req.group, { anyServerActive: true }), stdout: stdout?.trim() || "", stderr: stderr?.trim() || "", }); } catch (error) { console.error(`[servers] Failed to start ${server.id}:`, error); return res.status(500).json({ error: "Impossible de lancer le serveur" }); + } finally { + launchInProgress = false; } }); -function visibleServers(group) { +function serversWithImages() { const images = readServerImages(); - return mergeServersWithStatus(readServers()) - .map((server) => ({ - ...server, - heroImage: images[server.id]?.url || null, - })) - .filter((server) => canAccessServer(group, server.id)); + return mergeServersWithStatus(readServers()).map((server) => ({ + ...server, + heroImage: images[server.id]?.url || null, + })); } export { router as serversRouter }; diff --git a/src/services/permissions.js b/src/services/permissions.js index 934915c..999d0d9 100644 --- a/src/services/permissions.js +++ b/src/services/permissions.js @@ -48,7 +48,7 @@ function getPublicSecrets(secrets = {}) { ); } -function serializeServerForGroup(server, group) { +function serializeServerForGroup(server, group, { anyServerActive = false } = {}) { const publicSecrets = getPublicSecrets(server.secrets); const startProfile = getStartProfile(server); const serialized = { @@ -58,7 +58,8 @@ function serializeServerForGroup(server, group) { port: server.port, heroImage: server.heroImage || null, active: Boolean(server.active), - canStart: canAccessServer(group, server.id) && !server.active && Boolean(startProfile), + canStart: + canAccessServer(group, server.id) && !server.active && !anyServerActive && Boolean(startProfile), hasSecrets: Object.keys(publicSecrets).length > 0, }; diff --git a/test/api.test.js b/test/api.test.js index ea4e134..8635815 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -125,6 +125,7 @@ test("filters servers and shows secrets for visible non-admin servers", async () assert.equal(response.body.servers[0].id, "offline-server"); assert.equal(response.body.servers[0].secrets.password, "secret-1"); assert.equal(response.body.servers[0].hasSecrets, true); + assert.equal(response.body.servers[0].canStart, false); }); test("admin sees all servers and secrets", async () => { @@ -179,20 +180,50 @@ test("status file is authoritative for server list and names", async () => { }); test("starts an offline authorized server and writes a log", async () => { - const token = await login(playerPassword); - const start = await request("/api/servers/offline-server/start", { - method: "POST", - token, - }); + try { + fs.writeFileSync( + process.env.GAMESERVERS_STATUS_FILE, + `${JSON.stringify( + { + servers: [ + { + id: "offline-server", + game: "Offline Server", + address: "games.example:1111", + port: 1111, + active: false, + }, + { + id: "online-server", + game: "Online Server", + address: "games.example:2222", + port: 2222, + active: false, + }, + ], + }, + null, + 2 + )}\n` + ); - assert.equal(start.status, 200); - assert.equal(start.body.status, "started"); + const token = await login(playerPassword); + const start = await request("/api/servers/offline-server/start", { + method: "POST", + token, + }); - const adminToken = await login(adminLoginPassword); - const logs = await request("/api/logs", { token: adminToken }); - assert.equal(logs.status, 200); - assert.equal(logs.body.logs[0].groupId, "players"); - assert.equal(logs.body.logs[0].serverId, "offline-server"); + assert.equal(start.status, 200); + assert.equal(start.body.status, "started"); + + const adminToken = await login(adminLoginPassword); + const logs = await request("/api/logs", { token: adminToken }); + assert.equal(logs.status, 200); + assert.equal(logs.body.logs[0].groupId, "players"); + assert.equal(logs.body.logs[0].serverId, "offline-server"); + } finally { + fs.writeFileSync(process.env.GAMESERVERS_STATUS_FILE, originalStatus); + } }); test("refuses to start online or unauthorized servers", async () => { @@ -204,6 +235,13 @@ test("refuses to start online or unauthorized servers", async () => { assert.equal(unauthorized.status, 404); const adminToken = await login(adminLoginPassword); + const blockedByActiveServer = await request("/api/servers/offline-server/start", { + method: "POST", + token: adminToken, + }); + assert.equal(blockedByActiveServer.status, 409); + assert.equal(blockedByActiveServer.body.error, "Un serveur est deja en cours"); + const online = await request("/api/servers/online-server/start", { method: "POST", token: adminToken,