Compare commits
2 commits
b604ae64f9
...
119d5aad9d
| Author | SHA1 | Date | |
|---|---|---|---|
| 119d5aad9d | |||
| 655cf655da |
5 changed files with 124 additions and 24 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue