feat: allow only one server to be launched

This commit is contained in:
Shinuwa 2026-06-13 19:51:30 +02:00
parent e8805e0f2e
commit 655cf655da
5 changed files with 124 additions and 24 deletions

View file

@ -56,6 +56,16 @@
<p>Statut synchronise toutes les 5 minutes.</p> <p>Statut synchronise toutes les 5 minutes.</p>
</div> </div>
</div> </div>
<div class="info-banner">
<div>
<strong>Capacite limitee</strong>
<p>Un seul serveur de jeux peut etre lance a la fois afin d'eviter de surcharger le serveur.</p>
</div>
<div>
<strong>Arret automatique</strong>
<p>Les serveurs de jeux se ferment automatiquement si aucun joueur n'est present pendant 1 h.</p>
</div>
</div>
<div class="server-grid" id="serversBody"></div> <div class="server-grid" id="serversBody"></div>
</section> </section>

View file

@ -253,6 +253,36 @@ button {
margin-bottom: 1.2rem; 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, h2,
h3 { h3 {
margin: 0; margin: 0;
@ -704,6 +734,10 @@ code {
max-width: none; max-width: none;
} }
.info-banner {
grid-template-columns: 1fr;
}
.inline-form { .inline-form {
width: 100%; width: 100%;
} }

View file

@ -5,10 +5,17 @@ import { mergeServersWithStatus } from "../services/status.js";
import { getStartProfile, launchServer } from "../services/serverLauncher.js"; import { getStartProfile, launchServer } from "../services/serverLauncher.js";
const router = express.Router(); const router = express.Router();
let launchInProgress = false;
router.get("/servers", (req, res) => { router.get("/servers", (req, res) => {
const servers = visibleServers(req.group); const allServers = serversWithImages();
return res.json({ servers: servers.map((server) => serializeServerForGroup(server, req.group)) }); 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) => { 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" }); 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)) { if (!getStartProfile(server)) {
return res.status(400).json({ error: "Aucun profil de lancement configure" }); return res.status(400).json({ error: "Aucun profil de lancement configure" });
} }
launchInProgress = true;
try { try {
const { stdout, stderr } = await launchServer(server); const { stdout, stderr } = await launchServer(server);
@ -40,24 +57,24 @@ router.post("/servers/:serverId/start", async (req, res) => {
return res.json({ return res.json({
status: "started", status: "started",
server: serializeServerForGroup(server, req.group), server: serializeServerForGroup(server, req.group, { anyServerActive: true }),
stdout: stdout?.trim() || "", stdout: stdout?.trim() || "",
stderr: stderr?.trim() || "", stderr: stderr?.trim() || "",
}); });
} catch (error) { } catch (error) {
console.error(`[servers] Failed to start ${server.id}:`, error); console.error(`[servers] Failed to start ${server.id}:`, error);
return res.status(500).json({ error: "Impossible de lancer le serveur" }); return res.status(500).json({ error: "Impossible de lancer le serveur" });
} finally {
launchInProgress = false;
} }
}); });
function visibleServers(group) { function serversWithImages() {
const images = readServerImages(); const images = readServerImages();
return mergeServersWithStatus(readServers()) return mergeServersWithStatus(readServers()).map((server) => ({
.map((server) => ({ ...server,
...server, heroImage: images[server.id]?.url || null,
heroImage: images[server.id]?.url || null, }));
}))
.filter((server) => canAccessServer(group, server.id));
} }
export { router as serversRouter }; export { router as serversRouter };

View file

@ -48,7 +48,7 @@ function getPublicSecrets(secrets = {}) {
); );
} }
function serializeServerForGroup(server, group) { function serializeServerForGroup(server, group, { anyServerActive = false } = {}) {
const publicSecrets = getPublicSecrets(server.secrets); const publicSecrets = getPublicSecrets(server.secrets);
const startProfile = getStartProfile(server); const startProfile = getStartProfile(server);
const serialized = { const serialized = {
@ -58,7 +58,8 @@ function serializeServerForGroup(server, group) {
port: server.port, port: server.port,
heroImage: server.heroImage || null, heroImage: server.heroImage || null,
active: Boolean(server.active), 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, hasSecrets: Object.keys(publicSecrets).length > 0,
}; };

View file

@ -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].id, "offline-server");
assert.equal(response.body.servers[0].secrets.password, "secret-1"); assert.equal(response.body.servers[0].secrets.password, "secret-1");
assert.equal(response.body.servers[0].hasSecrets, true); assert.equal(response.body.servers[0].hasSecrets, true);
assert.equal(response.body.servers[0].canStart, false);
}); });
test("admin sees all servers and secrets", async () => { 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 () => { test("starts an offline authorized server and writes a log", async () => {
const token = await login(playerPassword); try {
const start = await request("/api/servers/offline-server/start", { fs.writeFileSync(
method: "POST", process.env.GAMESERVERS_STATUS_FILE,
token, `${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); const token = await login(playerPassword);
assert.equal(start.body.status, "started"); const start = await request("/api/servers/offline-server/start", {
method: "POST",
token,
});
const adminToken = await login(adminLoginPassword); assert.equal(start.status, 200);
const logs = await request("/api/logs", { token: adminToken }); assert.equal(start.body.status, "started");
assert.equal(logs.status, 200);
assert.equal(logs.body.logs[0].groupId, "players"); const adminToken = await login(adminLoginPassword);
assert.equal(logs.body.logs[0].serverId, "offline-server"); 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 () => { 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); assert.equal(unauthorized.status, 404);
const adminToken = await login(adminLoginPassword); 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", { const online = await request("/api/servers/online-server/start", {
method: "POST", method: "POST",
token: adminToken, token: adminToken,