Files
mountycrawl/admin.html
T

191 lines
7.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MountyCrawl — Administration du Monde Souterrain</title>
<link rel="stylesheet" href="css/style.css">
<style>
.admin-wrap { max-width: 1100px; margin: 0 auto; padding: 16px; }
.admin-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.admin-card { background: #FFFFEE; border: 1px solid #000; padding: 12px; }
.admin-card h2 { margin-top: 0; font-size: 1.05em; }
.admin-field { display: grid; grid-template-columns: 1fr 90px; gap: 6px; align-items: center; margin-bottom: 6px; }
.admin-field label { font-size: 0.85em; }
.admin-field small { display: block; color: #666; }
.admin-field input { width: 80px; padding: 3px; }
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.82em; }
.admin-table th, .admin-table td { border: 1px solid #999; padding: 2px 6px; text-align: left; }
.admin-log { max-height: 220px; overflow-y: auto; font-size: 0.82em; }
.admin-msg { font-weight: bold; margin: 8px 0; }
#admin-login { max-width: 480px; margin: 60px auto; }
</style>
</head>
<body>
<div class="admin-wrap">
<h1>⚙️ Administration du Monde Souterrain</h1>
<div id="admin-login" class="admin-card">
<h2>Token admin</h2>
<p><small>Le token est affiché dans les logs du serveur au démarrage
(ou dans <code>/data/admin-token.txt</code>), ou défini par la variable
d'environnement <code>MP_ADMIN_TOKEN</code>.</small></p>
<input type="password" id="admin-token" placeholder="token admin" style="width:100%">
<button id="admin-connect" class="big-btn">Entrer</button>
<div id="admin-login-msg" class="admin-msg"></div>
</div>
<div id="admin-body" class="hidden">
<div class="admin-msg" id="admin-msg"></div>
<div class="admin-grid">
<div class="admin-card">
<h2>⏱️ Réglages (appliqués à chaud)</h2>
<form id="admin-form"></form>
<button id="admin-save" class="big-btn">💾 Appliquer les réglages</button>
<hr>
<button id="admin-reset" class="menu-btn">🌋 Régénérer le monde (trolls conservés)</button>
</div>
<div class="admin-card">
<h2>📊 Vue d'ensemble</h2>
<div id="admin-overview"></div>
<h2>🌍 Derniers échos</h2>
<div id="admin-log" class="admin-log"></div>
</div>
</div>
<div class="admin-card" style="margin-top:16px">
<h2>🧌 Trolls</h2>
<div id="admin-trolls"></div>
</div>
<div class="admin-card" style="margin-top:16px">
<h2>👺 Monstres</h2>
<div id="admin-monsters"></div>
</div>
</div>
</div>
<script>
"use strict";
/* Libellés des réglages : [clé, libellé, description] */
const FIELDS = [
["monsterDlaMinSec", "DLA monstres — min (s)", "période minimale entre deux actions d'un monstre"],
["monsterDlaMaxSec", "DLA monstres — max (s)", "période maximale (chaque monstre tire sa DLA dans [min, max])"],
["trollDlaSec", "DLA des trolls (s)", "rechargement des PA des joueurs"],
["pollSec", "Rafraîchissement client (s)", "fréquence de polling conseillée aux navigateurs"],
["monsterTarget", "Population de monstres", "le repeuplement vise ce nombre"],
["repopSec", "Période de repeuplement (s)", "fréquence de vérification de la population"],
["itemTarget", "Trésors au sol", "nombre de trésors visé"],
["deathRespawnSec", "Réapparition des trolls (s)", "délai après une mort"],
["worldDepth", "Profondeur du monde (1-5)", "puissance des monstres et des trésors"],
["maxTrolls", "Trolls max", "limite de personnages dans le monde"],
["mapW", "Largeur de carte", "appliqué à la prochaine régénération du monde"],
["mapH", "Hauteur de carte", "appliqué à la prochaine régénération du monde"],
];
let TOKEN = localStorage.getItem("mc_admin_token") || "";
const $ = id => document.getElementById(id);
function fmtAgo(ms) {
const s = Math.round(ms / 1000);
if (s < 60) return s + " s";
if (s < 3600) return Math.round(s / 60) + " min";
return Math.round(s / 3600) + " h";
}
async function api(method, path, body) {
const res = await fetch(path, {
method,
headers: { "Content-Type": "application/json", "X-Admin-Token": TOKEN },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "erreur serveur");
return data;
}
function renderForm(config, bounds) {
const form = $("admin-form");
form.innerHTML = FIELDS.map(([key, label, desc]) => `
<div class="admin-field">
<label>${label}<small>${desc} (${bounds[key][0]}${bounds[key][1]})</small></label>
<input type="number" name="${key}" value="${config[key]}" min="${bounds[key][0]}" max="${bounds[key][1]}">
</div>`).join("");
}
function renderOverview(ov) {
$("admin-overview").innerHTML = `
<p>Uptime du monde : <b>${fmtAgo(ov.uptime)}</b> ·
Trolls : <b>${ov.trolls.length}</b> ·
Monstres : <b>${ov.monsters.length}</b> ·
Trésors au sol : <b>${ov.items}</b></p>`;
$("admin-log").innerHTML = ov.logTail.map(l =>
`<div>${new Date(l.t).toLocaleTimeString("fr-FR")}${l.msg}</div>`).reverse().join("");
$("admin-trolls").innerHTML = ov.trolls.length ? `<table class="admin-table">
<tr><th>Nom</th><th>Race</th><th>Niv.</th><th>PV</th><th>PA</th><th>Kills</th><th>Or</th><th>Position</th><th>État</th><th>Vu il y a</th></tr>
${ov.trolls.map(t => `<tr>
<td>${t.name}</td><td>${t.race}</td><td>${t.level}</td>
<td>${t.pv}/${t.pvMax}</td><td>${t.pa}</td><td>${t.kills}</td><td>${t.gold}</td>
<td>${t.x},${t.y}</td><td>${t.dead ? "☠️ mort" : "vivant"}</td><td>${fmtAgo(t.lastSeenAgo)}</td>
</tr>`).join("")}</table>` : "<p>Aucun troll pour l'instant.</p>";
$("admin-monsters").innerHTML = `<table class="admin-table">
<tr><th></th><th>Nom</th><th>Niv.</th><th>PV</th><th>Position</th><th>DLA (s)</th><th>Prochaine action</th></tr>
${ov.monsters.map(m => `<tr>
<td>${m.emoji}</td><td>${m.name}</td><td>${m.level}</td>
<td>${m.pv}/${m.pvMax}</td><td>${m.x},${m.y}</td>
<td>${m.dlaSec}</td><td>dans ${Math.round(m.nextDlaIn / 1000)} s</td>
</tr>`).join("")}</table>`;
}
async function refresh(withForm = false) {
const ov = await api("GET", "api/mp/admin");
if (withForm) renderForm(ov.config, ov.bounds);
renderOverview(ov);
}
async function connect() {
try {
await refresh(true);
localStorage.setItem("mc_admin_token", TOKEN);
$("admin-login").classList.add("hidden");
$("admin-body").classList.remove("hidden");
setInterval(() => refresh(false).catch(() => {}), 10000);
} catch (e) {
$("admin-login-msg").textContent = "⚠️ " + e.message;
}
}
document.addEventListener("DOMContentLoaded", () => {
$("admin-connect").onclick = () => {
TOKEN = $("admin-token").value.trim();
connect();
};
$("admin-save").onclick = async () => {
const patch = {};
for (const input of $("admin-form").querySelectorAll("input")) patch[input.name] = input.value;
try {
await api("PUT", "api/mp/admin/config", patch);
$("admin-msg").textContent = "✅ Réglages appliqués à chaud.";
await refresh(true);
} catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; }
};
$("admin-reset").onclick = async () => {
if (!confirm("Régénérer la carte, les monstres et les trésors ? (les trolls sont conservés et replacés)")) return;
try {
await api("POST", "api/mp/admin/reset");
$("admin-msg").textContent = "🌋 Monde régénéré.";
await refresh(true);
} catch (e) { $("admin-msg").textContent = "⚠️ " + e.message; }
};
// ?token=… dans l'URL : connexion directe (le token est ensuite mémorisé)
const urlToken = new URLSearchParams(location.search).get("token");
if (urlToken) TOKEN = urlToken;
if (TOKEN) { $("admin-token").value = TOKEN; connect(); }
});
</script>
</body>
</html>