"use strict";
const API = {
state: "/z-panel/state",
setup: "/z-panel/setup",
login: "/z-panel/login",
me: "/z-panel/me",
stats: "/z-panel/api/stats",
timeline: "/z-panel/api/timeline",
logs: "/z-panel/api/logs",
system: "/z-panel/api/system",
restart: "/z-panel/api/restart",
articles: "/z-panel/api/articles",
i18n: "/z-panel/api/i18n",
rules: "/z-panel/api/rules",
honeypot: "/z-panel/api/honeypot",
zmap: "/z-panel/api/zmap",
};
const TOKEN_KEY = "zektyc_admin_token";
const REFRESH_MS = 15000;
const $ = (id) => document.getElementById(id);
let authMode = "login";
let token = sessionStorage.getItem(TOKEN_KEY) || "";
let autoTimer = null;
let logTimer = null;
function authHeaders() {
return token ? { Authorization: "Bearer " + token } : {};
}
async function api(path, opts = {}) {
const headers = Object.assign({}, opts.headers || {}, authHeaders());
if (opts.body) headers["Content-Type"] = "application/json";
const res = await fetch(path, Object.assign({}, opts, { headers }));
let data = null;
try {
data = await res.json();
} catch {}
if (res.status === 401 && !opts._noAuth) {
clearSession();
throw new Error("Session expirée");
}
return { status: res.status, data };
}
function clearSession() {
token = "";
sessionStorage.removeItem(TOKEN_KEY);
$("app").hidden = true;
$("login").hidden = false;
setAuthMode("login");
}
function fmtDate(ts) {
const d = new Date(ts * 1000);
const p = (n) => String(n).padStart(2, "0");
return `${p(d.getDate())}/${p(d.getMonth() + 1)} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
function fmtBytes(n) {
if (n == null) return "-";
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " Ko";
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + " Mo";
return (n / 1024 / 1024 / 1024).toFixed(2) + " Go";
}
function fmtUptime(s) {
if (s == null) return "-";
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
if (d > 0) return `${d} j ${h} h ${m} min`;
if (h > 0) return `${h} h ${m} min`;
return `${m} min`;
}
function pillClass(status) {
const s = Number(status);
if (s >= 500) return "s5xx";
if (s >= 400) return "s4xx";
if (s >= 300) return "s3xx";
if (s >= 200) return "s2xx";
return "s0";
}
function esc(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
);
}
/* ---------- Authentification ---------- */
function setAuthMode(mode) {
authMode = mode;
const btn = $("auth-btn");
if (mode === "setup") {
$("auth-form").querySelector("label").textContent = "Définir un mot de passe";
btn.textContent = "Configurer et se connecter";
$("password").autocomplete = "new-password";
} else {
$("auth-form").querySelector("label").textContent = "Mot de passe";
btn.textContent = "Connexion";
$("password").autocomplete = "current-password";
}
}
function setMsg(text, cls) {
const m = $("auth-msg");
m.textContent = text;
m.className = "msg" + (cls ? " " + cls : "");
}
async function initAuth() {
$("app").hidden = true;
$("login").hidden = false;
if (token) {
const r = await api(API.me, { _noAuth: true });
if (r.status === 200) {
$("login").hidden = true;
$("app").hidden = false;
startAutoRefresh();
return;
}
token = "";
sessionStorage.removeItem(TOKEN_KEY);
}
const st = await api(API.state, { _noAuth: true });
if (st.status !== 200) {
setMsg("Impossible de contacter le serveur.", "err");
return;
}
setAuthMode(st.data && st.data.configured ? "login" : "setup");
if (st.data && st.data.configured === false) {
setMsg("Premier lancement : définissez votre mot de passe.", "");
}
}
async function submitAuth(e) {
e.preventDefault();
const password = $("password").value;
setMsg("", "");
$("auth-btn").disabled = true;
try {
if (authMode === "setup") {
const s = await api(API.setup, { method: "POST", body: JSON.stringify({ password }) });
if (s.status !== 200) {
setMsg(s.data && s.data.error ? s.data.error : "Erreur", "err");
return;
}
}
const r = await api(API.login, { method: "POST", body: JSON.stringify({ password }) });
if (r.status === 429) {
setMsg("Trop de tentatives. Réessayez dans quelques minutes.", "err");
return;
}
if (r.status === 401) {
setMsg("Mot de passe incorrect.", "err");
return;
}
if (r.status === 409) {
setAuthMode("login");
setMsg("Le mot de passe a déjà été configuré. Connectez-vous.", "err");
return;
}
if (r.status !== 200) {
setMsg(r.data && r.data.error ? r.data.error : "Erreur", "err");
return;
}
token = r.data.token;
sessionStorage.setItem(TOKEN_KEY, token);
$("password").value = "";
setMsg("", "");
$("login").hidden = true;
$("app").hidden = false;
startAutoRefresh();
} catch (err) {
setMsg(err.message || "Erreur réseau", "err");
} finally {
$("auth-btn").disabled = false;
}
}
/* ---------- Navigation par onglets ---------- */
const TITLES = {
dash: "Tableau de bord",
logs: "Logs",
system: "Système",
articles: "Articles",
trans: "Traductions",
rules: "Règles Discord",
honeypot: "Honeypot",
zmap: "ZMap",
restart: "Redémarrage",
};
function switchTab(name) {
for (const b of document.querySelectorAll(".nav-item")) {
b.classList.toggle("active", b.dataset.tab === name);
}
for (const sec of document.querySelectorAll(".tab")) {
sec.hidden = sec.id !== "tab-" + name;
}
$("page-title").textContent = TITLES[name];
if (name === "logs") loadLogs();
if (name === "system") loadSystem();
if (name === "zmap") loadZmap();
if (name === "articles") loadArticles();
if (name === "trans") loadTranslations();
if (name === "rules") loadRules();
if (name === "honeypot") loadHoneypot();
}
/* ---------- Tableau de bord ---------- */
async function loadDashboard() {
const [statsRes, tlRes] = await Promise.all([api(API.stats), api(API.timeline)]);
if (statsRes.status !== 200 || tlRes.status !== 200) throw new Error("stats");
renderStats(statsRes.data);
renderTimeline(tlRes.data);
}
function renderStats(s) {
const total = s.total || 0;
const by = s.byStatus || {};
const cards = [
[total, "requêtes", "stat-value"],
[by["200"] ?? 0, "200 OK", "stat-value"],
[by["404"] ?? 0, "404 bloqués", "stat-value"],
[by["429"] ?? 0, "429 rate-limit", "stat-value"],
[by["405"] ?? 0, "405 refusés", "stat-value"],
[Object.keys(by).length, "statuts distincts", "stat-value"],
];
document.querySelectorAll("#stat-cards .stat-card").forEach((card, i) => {
const [v, label] = cards[i];
card.querySelector(".stat-value").textContent = v.toLocaleString("fr-FR");
card.querySelector(".stat-label").textContent = label;
});
const blocked = s.blocked || [];
$("blocked-count").textContent = blocked.length + " cibles";
if (blocked.length === 0) {
$("blocked-table").innerHTML = '
Aucun 404.
';
} else {
$("blocked-table").innerHTML =
"| Chemin | Hits |
" +
blocked
.map(
([uri, n]) =>
`| ${esc(uri)} | ${n.toLocaleString("fr-FR")} |
`
)
.join("") +
"
";
}
const by429 = Object.entries(s.by429 || {}).sort((a, b) => b[1] - a[1]).slice(0, 10);
$("ratelimit-count").textContent = by429.length + " IP";
if (by429.length === 0) {
$("ratelimit-table").innerHTML = 'Aucun 429.
';
} else {
$("ratelimit-table").innerHTML =
"| IP | Limitations |
" +
by429
.map(
([ip, n]) =>
`| ${esc(ip)} | ${n.toLocaleString("fr-FR")} |
`
)
.join("") +
"
";
}
}
function renderTimeline(data) {
const W = 900;
const H = 160;
const padB = 26;
const padT = 8;
const n = data.length;
const max = Math.max.apply(null, data.map((d) => d.total).concat([1]));
const band = W / n;
const bw = Math.max(band * 0.6, 1);
const plotH = H - padB - padT;
const seg = (v) => Math.round((v / max) * plotH);
let rects = "";
let labels = "";
data.forEach((d, i) => {
const x = i * band + (band - bw) / 2;
let y = padT + plotH;
const stack = [];
if (d.ok) stack.push(["#34d399", d.ok]);
if (d.redir) stack.push(["#58a6ff", d.redir]);
if (d.err4) stack.push(["#fbbf24", d.err4]);
if (d.err5) stack.push(["#f87171", d.err5]);
if (d.total === 0) {
rects += ``;
}
const tooltip =
`${fmtDate(d.hour)} : ${d.total} req` +
(d.ok ? ` · ${d.ok} OK` : "") +
(d.err4 ? ` · ${d.err4} 4xx` : "") +
(d.err5 ? ` · ${d.err5} 5xx` : "") +
``;
for (const [color, v] of stack) {
const h = seg(v);
y -= h;
rects += `${tooltip}`;
}
if (i % 6 === 0) {
const h = new Date(d.hour * 1000).getHours();
labels += `${h}h`;
}
});
const first = data[0];
const last = data[n - 1];
$("chart-meta").textContent = `${fmtDate(first.hour)} → ${fmtDate(last.hour)}`;
$("chart").innerHTML = n
? ``
: 'Aucune donnée.
';
}
/* ---------- Logs ---------- */
let logFilterTimer = null;
const LOG_EXCLUDE_KEY = "zektyc_log_exclude";
let logExclude = localStorage.getItem(LOG_EXCLUDE_KEY) !== "0";
function renderLogExclude() {
const btn = $("log-exclude");
btn.setAttribute("aria-pressed", String(logExclude));
btn.classList.toggle("active", logExclude);
$("log-exclude-label").textContent = logExclude ? "Masquer le bruit" : "Tout afficher";
}
function setLogExclude(v) {
logExclude = v;
localStorage.setItem(LOG_EXCLUDE_KEY, v ? "1" : "0");
renderLogExclude();
loadLogs();
}
async function loadLogs() {
const filter = $("log-filter").value.trim();
const status = $("log-status").value;
const q = new URLSearchParams({ lines: "300" });
if (filter) q.set("filter", filter);
if (status) q.set("status", status);
if (logExclude) q.set("exclude", "1");
const r = await api(API.logs + "?" + q.toString());
if (r.status !== 200) return;
const { entries, total, truncated, returned } = r.data;
$("log-meta").textContent =
`${returned} affichées · ${total.toLocaleString("fr-FR")} au total` + (truncated ? " · tronqué" : "");
const tbody = $("log-table").querySelector("tbody");
if (entries.length === 0) {
tbody.innerHTML = '| Aucune entrée. |
';
return;
}
tbody.innerHTML = entries
.slice()
.reverse()
.map((e) => {
const d =
typeof e.duration === "number"
? (e.duration < 100 ? e.duration.toFixed(1) : Math.round(e.duration)) + " ms"
: "-";
const ua = (e.ua || "-").length > 60 ? (e.ua || "-").slice(0, 60) + "…" : e.ua || "-";
return `
| ${fmtDate(e.ts)} |
${esc(e.ip)} |
${esc(e.method)} |
${esc(e.uri)} |
${esc(e.status)} |
${d} |
${esc(ua)} |
`;
})
.join("");
}
/* ---------- Système ---------- */
function fmtLoadAvg(la, cpus) {
if (!la) return "-";
const parts = la.split(" ");
const cores = typeof cpus === "number" ? cpus : parts.length > 3 ? parts[3].split("/")[1] : null;
return `${parts[0]} / ${parts[1]} / ${parts[2]}` + (cores ? ` (${cores} cœur${cores > 1 ? "s" : ""})` : "");
}
async function loadSystem() {
const r = await api(API.system);
if (r.status !== 200) return;
const s = r.data;
const mem = s.mem || {};
const total = mem.MemTotal || 0;
const available = mem.MemAvailable ?? total;
const used = Math.max(total - available, 0);
const pct = total ? Math.round((used / total) * 100) : 0;
const cards = [
[fmtUptime(s.uptime), "uptime"],
[fmtLoadAvg(s.loadavg, s.cpus), "charge CPU (1/5/15)"],
[`${pct} % · ${fmtBytes(used)}`, "mémoire utilisée"],
[String(s.procs ? s.procs.length : 0), "processus suivis"],
];
$("sys-grid").innerHTML = cards
.map(
([v, label]) => ``
)
.join("");
if (!s.procs || s.procs.length === 0) {
$("procs").innerHTML = 'Aucun processus.
';
} else {
$("procs").innerHTML =
"| Nom | PID | RAM | Commande |
" +
s.procs
.sort((a, b) => (b.rss || 0) - (a.rss || 0))
.map(
(p) =>
`| ${esc(p.name)} | ${p.pid} | ${fmtBytes(p.rss)} | ${esc(p.cmd)} |
`
)
.join("") +
"
";
}
if (!s.disk) {
$("disk").innerHTML = 'Indisponible.
';
} else {
const rows = s.disk.slice(1).map((line) => line.trim().split(/\s+/));
$("disk").innerHTML =
"| Fichier | Taille | Utilisé | Disponible | % | Monté sur |
" +
rows
.map(
(c) =>
`| ${esc(c[0])} | ${fmtBytes(Number(c[1]) * 1024)} | ${fmtBytes(Number(c[2]) * 1024)} | ${fmtBytes(Number(c[3]) * 1024)} | ${esc(c[4])} | ${esc(c[5])} |
`
)
.join("") +
"
";
}
}
/* ---------- ZMap ---------- */
async function loadZmap() {
const r = await api(API.zmap);
if (r.status !== 200) return;
const d = r.data;
const b = d.build || {};
const grids = d.grids || {};
// stat cards
const bRunning = b.running;
const bPass = b.done ? "terminé" : b.pass ? `pass ${b.pass}` : "—";
const bWays = b.ways ? `${(b.ways / 1e6).toFixed(1)}M` : "—";
const bNodes = b.nodes ? `${(b.nodes / 1e6).toFixed(1)}M` : "—";
const bRss = b.rss ? fmtBytes(b.rss) : "—";
const cards = [
[bRunning ? "En cours" : (b.done ? "Terminé" : "Arrêté"), "état build"],
[bPass, "pass actuel"],
[bWays, "ways traités"],
[bNodes, "nœuds collectés"],
[bRss, "RAM build"],
[grids.europe ? fmtBytes(grids.europe.totalSize) : "—", "grille Europe"],
[grids.planet ? fmtBytes(grids.planet.totalSize) : "—", "grille Monde"],
];
$("zmap-grid").innerHTML = cards.map(([v, label]) =>
``
).join("");
// build info
$("zmap-build-meta").textContent = b.pid ? `PID ${b.pid}` : "";
if (b.done) {
$("zmap-build-info").innerHTML = 'Build terminé.
';
} else if (!b.running) {
$("zmap-build-info").innerHTML = 'Aucun build en cours.
';
} else {
const pctWays = b.ways ? Math.min(100, Math.round((b.ways / 50e6) * 100)) : 0;
const pctNodes = b.nodes ? Math.min(100, Math.round((b.nodes / 500e6) * 100)) : 0;
const bar = (pct, label) => ``;
$("zmap-build-info").innerHTML =
`${bar(pctWays, "Ways (~50M)")}${bar(pctNodes, "Nœuds (~500M)")}
`;
}
// grids
$("zmap-grids-meta").textContent = "";
const gNames = { europe: "Europe", planet: "Monde" };
let gHtml = '| Grille | Points | Cellules | Taille gzip | Couverture |
';
for (const [key, label] of Object.entries(gNames)) {
const g = grids[key];
if (g) {
const cover = (g.lonMin != null) ? `${g.lonMin}°…${g.lonMax}° lon, ${g.latMin}°…${g.latMax}° lat` : "—";
gHtml += `| ${esc(label)} | ${g.points?.toLocaleString("fr-FR") ?? "—"} | ${g.cells?.toLocaleString("fr-FR") ?? "—"} | ${fmtBytes(g.totalSize)} | ${esc(cover)} |
`;
} else {
gHtml += `| ${esc(label)} | pas encore construite |
`;
}
}
gHtml += '
';
$("zmap-grids-info").innerHTML = gHtml;
// log
const logLines = b.log || [];
$("zmap-build-log").textContent = logLines.join("\n") || "Aucun log.";
}
/* ---------- Articles ---------- */
let articleSlug = null;
function setArticleMsg(text, cls) {
const m = $("art-msg");
m.textContent = text;
m.className = "msg" + (cls ? " " + cls : "");
}
async function loadArticles() {
const r = await api(API.articles);
if (r.status !== 200) return;
const list = r.data || [];
$("articles-meta").textContent = list.length + " article" + (list.length > 1 ? "s" : "");
if (list.length === 0) {
$("articles-list").innerHTML = 'Aucun article. Créez le premier.
';
return;
}
$("articles-list").innerHTML =
"| Slug | Titre FR | Titre EN | Publié | Modifié | Actions |
" +
list
.map(
(a) =>
`
| ${esc(a.slug)} |
${esc(a.frTitle)} |
${esc(a.enTitle)} |
${esc(a.published)} |
${esc(a.modified)} |
|
`
)
.join("") +
"
";
}
function fillArticleForm(article) {
$("art-slug").value = article.slug;
$("art-published").value = article.published || "";
$("art-fr-title").value = (article.fr && article.fr.title) || "";
$("art-fr-og").value = (article.fr && article.fr.ogTitle) || "";
$("art-fr-desc").value = (article.fr && article.fr.description) || "";
$("art-fr-body").value = (article.fr && article.fr.body) || "";
$("art-en-title").value = (article.en && article.en.title) || "";
$("art-en-og").value = (article.en && article.en.ogTitle) || "";
$("art-en-desc").value = (article.en && article.en.description) || "";
$("art-en-body").value = (article.en && article.en.body) || "";
articleSlug = article.slug;
$("article-editor-title").textContent = "Modifier l'article";
$("article-editor-meta").textContent = "/actualites/" + article.slug + "/";
$("art-delete").hidden = false;
$("article-editor").hidden = false;
setArticleMsg("", "");
$("article-editor").scrollIntoView({ behavior: "smooth", block: "start" });
}
async function openArticleEditor(slug) {
if (slug) {
const r = await api(API.articles + "/" + slug);
if (r.status !== 200) return;
fillArticleForm(r.data);
return;
}
for (const id of [
"art-slug",
"art-fr-title",
"art-fr-og",
"art-fr-desc",
"art-fr-body",
"art-en-title",
"art-en-og",
"art-en-desc",
"art-en-body",
]) {
$(id).value = "";
}
$("art-published").value = new Date().toISOString().slice(0, 10);
articleSlug = null;
$("article-editor-title").textContent = "Nouvel article";
$("article-editor-meta").textContent = "";
$("art-delete").hidden = true;
$("article-editor").hidden = false;
setArticleMsg("", "");
$("article-editor").scrollIntoView({ behavior: "smooth", block: "start" });
}
function collectArticleForm() {
return {
slug: $("art-slug").value.trim(),
published: $("art-published").value,
fr: {
title: $("art-fr-title").value,
ogTitle: $("art-fr-og").value,
description: $("art-fr-desc").value,
body: $("art-fr-body").value,
},
en: {
title: $("art-en-title").value,
ogTitle: $("art-en-og").value,
description: $("art-en-desc").value,
body: $("art-en-body").value,
},
};
}
function copyLangFields(from, to) {
$("art-" + to + "-title").value = $("art-" + from + "-title").value;
$("art-" + to + "-og").value = $("art-" + from + "-og").value;
$("art-" + to + "-desc").value = $("art-" + from + "-desc").value;
$("art-" + to + "-body").value = $("art-" + from + "-body").value;
setArticleMsg(
from === "fr"
? "Contenu français copié en anglais — traduis puis publie."
: "Contenu anglais copié en français — traduis puis publie.",
"ok"
);
}
function togglePreview(lang) {
const pv = $("art-" + lang + "-preview");
if (pv.hidden) {
const ta = $("art-" + lang + "-body");
pv.innerHTML = window.marked ? window.marked.parse(ta.value || "") : "";
pv.hidden = false;
} else {
pv.hidden = true;
}
}
async function saveArticle(e) {
e.preventDefault();
const body = collectArticleForm();
if (!/^[a-z0-9-]+$/.test(body.slug)) {
setArticleMsg("Slug invalide : minuscules a-z, chiffres et tirets uniquement.", "err");
return;
}
if (!body.published) {
setArticleMsg("Date de publication requise.", "err");
return;
}
if (
!body.fr.title.trim() ||
!body.en.title.trim() ||
!body.fr.body.trim() ||
!body.en.body.trim()
) {
setArticleMsg("Les titres et contenus FR et EN sont requis.", "err");
return;
}
setArticleMsg("", "");
$("art-save").disabled = true;
try {
const method = articleSlug ? "PUT" : "POST";
const r = await api(API.articles, { method, body: JSON.stringify(body) });
if (r.status !== 200 && r.status !== 201) {
setArticleMsg(r.data && r.data.error ? r.data.error : "Erreur", "err");
return;
}
setArticleMsg(articleSlug ? "Article mis à jour." : "Article créé.", "ok");
$("article-editor").hidden = true;
loadArticles();
} catch (err) {
if (err && err.message === "Session expirée") return;
setArticleMsg("Erreur réseau", "err");
} finally {
$("art-save").disabled = false;
}
}
async function deleteArticle(slug) {
if (!window.confirm(`Supprimer l'article « ${slug} » ? Cette action est définitive.`)) return;
const r = await api(API.articles + "/" + slug, { method: "DELETE" });
if (r.status !== 200) {
if (r.status === 401) return;
window.alert(r.data && r.data.error ? r.data.error : "Erreur");
return;
}
if (articleSlug === slug) $("article-editor").hidden = true;
loadArticles();
}
/* ---------- Traductions ---------- */
let transKeys = [];
function setTransMsg(text, cls) {
const m = $("trans-msg");
m.textContent = text;
m.className = "msg" + (cls ? " " + cls : "");
}
function autoGrow(ta) {
ta.style.height = "auto";
ta.style.height = Math.max(ta.scrollHeight + 4, 40) + "px";
}
function transRow(key, fr, en) {
const tr = document.createElement("tr");
tr.dataset.key = key;
tr.innerHTML =
`${esc(key)} | ` +
` | ` +
` | ` +
` | `;
tr.querySelectorAll("textarea").forEach((ta) => {
ta.addEventListener("input", () => autoGrow(ta));
autoGrow(ta);
});
tr.querySelector(".trans-del").addEventListener("click", () => {
tr.remove();
setTransMsg("Modifications non enregistrées.", "ok");
updateTransCount();
});
return tr;
}
function applyTransFilter() {
const q = $("trans-filter").value.trim().toLowerCase();
for (const tr of $("trans-table").querySelectorAll("tbody tr")) {
tr.hidden = !!(q && !tr.dataset.key.includes(q));
}
updateTransCount();
}
function updateTransCount() {
const total = transKeys.length;
const visible = $("trans-table").querySelectorAll("tbody tr:not([hidden])").length;
$("trans-meta").textContent = visible + " / " + total + " clés";
}
async function loadTranslations() {
const r = await api(API.i18n);
if (r.status !== 200) return;
const dict = r.data || { fr: {}, en: {} };
transKeys = Array.from(
new Set([...Object.keys(dict.fr || {}), ...Object.keys(dict.en || {})])
).sort();
const tbody = $("trans-table").querySelector("tbody");
tbody.innerHTML = "";
const frag = document.createDocumentFragment();
for (const key of transKeys) {
frag.appendChild(transRow(key, (dict.fr || {})[key] || "", (dict.en || {})[key] || ""));
}
tbody.appendChild(frag);
updateTransCount();
setTransMsg("", "");
}
async function saveTranslations() {
const dict = { fr: {}, en: {} };
for (const tr of $("trans-table").querySelectorAll("tbody tr")) {
const key = tr.dataset.key;
dict.fr[key] = tr.querySelector('[data-lang="fr"]').value;
dict.en[key] = tr.querySelector('[data-lang="en"]').value;
}
setTransMsg("", "");
$("trans-save").disabled = true;
try {
const r = await api(API.i18n, { method: "PUT", body: JSON.stringify(dict) });
if (r.status !== 200) {
setTransMsg(r.data && r.data.error ? r.data.error : "Erreur", "err");
return;
}
setTransMsg("Traductions enregistrées.", "ok");
loadTranslations();
} catch (err) {
if (err && err.message === "Session expirée") return;
setTransMsg("Erreur réseau", "err");
} finally {
$("trans-save").disabled = false;
}
}
function addTransKey() {
const key = window.prompt("Clé de traduction (ex : faq.q9) :");
if (!key) return;
const k = key.trim();
if (!k) return;
const tbody = $("trans-table").querySelector("tbody");
if ([...tbody.children].some((tr) => tr.dataset.key === k)) {
setTransMsg("Cette clé existe déjà.", "err");
return;
}
tbody.appendChild(transRow(k, "", ""));
transKeys.push(k);
transKeys.sort();
$("trans-filter").value = "";
applyTransFilter();
setTransMsg("Clé ajoutée, enregistrez pour appliquer.", "ok");
}
/* ---------- Règles Discord ---------- */
async function loadRules() {
const r = await api(API.rules);
if (r.status !== 200) return;
$("rules-content").value = (r.data && r.data.content) || "";
const len = (r.data && r.data.length) || 0;
$("rules-meta").textContent = len ? len + " caractères" : "";
setRulesMsg("", "");
}
function setRulesMsg(text, cls) {
const m = $("rules-msg");
m.textContent = text;
m.className = "msg" + (cls ? " " + cls : "");
}
async function saveRules() {
const content = $("rules-content").value;
setRulesMsg("", "");
$("rules-save").disabled = true;
try {
const r = await api(API.rules, { method: "PUT", body: JSON.stringify({ content }) });
if (r.status !== 200) {
setRulesMsg(r.data && r.data.error ? r.data.error : "Erreur", "err");
return;
}
setRulesMsg("Règles enregistrées — le bot les publiera dans la minute.", "ok");
loadRules();
} catch (err) {
if (err && err.message === "Session expirée") return;
setRulesMsg("Erreur réseau", "err");
} finally {
$("rules-save").disabled = false;
}
}
/* ---------- Honeypot ---------- */
function hpEndpointsHtml(endpoints) {
const top = Object.entries(endpoints || {})
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
if (top.length === 0) return '—';
return top
.map(([p, c]) => `${esc(p)} ×${c}`)
.join(" ");
}
async function loadHoneypot() {
const r = await api(API.honeypot);
if (r.status !== 200) return;
const d = r.data || {};
const ips = d.ips || [];
const total = d.total || 0;
const banned = ips.filter((x) => x.banned).length;
$("honeypot-meta").textContent =
total + " IP piégées" + (banned ? " · " + banned + " bannie(s)" : "") + " (max " + (d.max || "-") + ")";
const box = $("honeypot-list");
if (ips.length === 0) {
box.innerHTML = 'Aucune IP piégée pour le moment.
';
return;
}
box.innerHTML =
'| IP | Hits | Chemins | Premier | Dernier | Statut | |
' +
ips
.map((e) => {
const badge = e.banned ? 'Bannie' : 'Active';
return (
"" +
`| ${esc(e.ip)} | ` +
`${e.count} | ` +
`${hpEndpointsHtml(e.endpoints)} | ` +
`${fmtDate(Math.floor(e.first / 1000))} | ` +
`${fmtDate(Math.floor(e.last / 1000))} | ` +
`${badge} | ` +
` | ` +
"
"
);
})
.join("") +
"
";
}
async function setHoneypotBan(ip, ban) {
const r = await api(API.honeypot, { method: "POST", body: JSON.stringify({ action: ban ? "ban" : "unban", ip }) });
if (r.status !== 200) return;
loadHoneypot();
}
async function clearHoneypot() {
if (!confirm("Vider toute la liste honeypot ?")) return;
const r = await api(API.honeypot, { method: "POST", body: JSON.stringify({ action: "clear" }) });
if (r.status !== 200) return;
loadHoneypot();
}
/* ---------- Actualisation ---------- */
async function refreshAll() {
try {
await loadDashboard();
$("updated").textContent = "à jour " + new Date().toLocaleTimeString("fr-FR", { hour12: false });
} catch (err) {
if (err && err.message === "Session expirée") return;
$("updated").textContent = "erreur de rafraîchissement";
}
}
function startAutoRefresh() {
stopAutoRefresh();
autoTimer = setInterval(refreshAll, REFRESH_MS);
logTimer = setInterval(() => {
if (!$("tab-logs").hidden) loadLogs();
if (!$("tab-system").hidden) loadSystem();
if (!$("tab-zmap").hidden) loadZmap();
}, REFRESH_MS);
refreshAll();
}
function stopAutoRefresh() {
if (autoTimer) clearInterval(autoTimer);
if (logTimer) clearInterval(logTimer);
autoTimer = null;
logTimer = null;
}
/* ---------- Redémarrage ---------- */
async function doRestart(service) {
const label = service === "app" ? "l'application" : "Caddy";
if (!window.confirm(`Redémarrer ${label} ?`)) return;
const btn = service === "app" ? $("restart-app") : $("restart-caddy");
btn.disabled = true;
btn.textContent = "Redémarrage…";
try {
const r = await api(API.restart, { method: "POST", body: JSON.stringify({ service }) });
if (r.status !== 200) {
window.alert(r.data && r.data.error ? r.data.error : "Erreur");
return;
}
if (service === "app") {
setTimeout(() => {
window.alert("Application redémarrée. Votre session est expirée, reconnectez-vous.");
clearSession();
}, 2000);
} else {
setTimeout(() => window.alert("Caddy redémarré."), 2000);
}
} catch (err) {
if (err && err.message === "Session expirée") return;
window.alert("Erreur réseau");
} finally {
btn.disabled = false;
btn.textContent = label === "l'application" ? "Redémarrer l'application" : "Redémarrer Caddy";
}
}
/* ---------- Câblage ---------- */
$("auth-form").addEventListener("submit", submitAuth);
$("toggle-pw").addEventListener("click", () => {
const input = $("password");
input.type = input.type === "password" ? "text" : "password";
});
document.querySelectorAll(".nav-item").forEach((b) => b.addEventListener("click", () => switchTab(b.dataset.tab)));
$("refresh").addEventListener("click", refreshAll);
$("logout").addEventListener("click", () => {
stopAutoRefresh();
clearSession();
setMsg("", "");
});
$("autorefresh").addEventListener("change", (e) => {
if (e.target.checked) startAutoRefresh();
else stopAutoRefresh();
});
$("restart-app").addEventListener("click", () => doRestart("app"));
$("restart-caddy").addEventListener("click", () => doRestart("caddy"));
$("log-filter").addEventListener("input", () => {
if (logFilterTimer) clearTimeout(logFilterTimer);
logFilterTimer = setTimeout(loadLogs, 350);
});
$("log-status").addEventListener("change", loadLogs);
$("log-exclude").addEventListener("click", () => setLogExclude(!logExclude));
$("article-new").addEventListener("click", () => openArticleEditor());
$("article-form").addEventListener("submit", saveArticle);
$("art-cancel").addEventListener("click", () => {
$("article-editor").hidden = true;
setArticleMsg("", "");
});
$("art-delete").addEventListener("click", () => {
if (articleSlug) deleteArticle(articleSlug);
});
$("articles-list").addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const slug = btn.dataset.slug;
if (btn.dataset.action === "edit") openArticleEditor(slug);
else if (btn.dataset.action === "view") window.open("/actualites/" + slug + "/", "_blank", "noopener");
else if (btn.dataset.action === "delete") deleteArticle(slug);
});
$("trans-save").addEventListener("click", saveTranslations);
$("trans-add").addEventListener("click", addTransKey);
$("trans-filter").addEventListener("input", applyTransFilter);
$("rules-save").addEventListener("click", saveRules);
$("rules-reload").addEventListener("click", loadRules);
$("honeypot-refresh").addEventListener("click", loadHoneypot);
$("honeypot-clear").addEventListener("click", clearHoneypot);
$("honeypot-list").addEventListener("click", (e) => {
const btn = e.target.closest("[data-hp]");
if (!btn) return;
setHoneypotBan(btn.dataset.hp, btn.dataset.ban === "1");
});
$("art-copy-from-en").addEventListener("click", () => copyLangFields("en", "fr"));
$("art-copy-from-fr").addEventListener("click", () => copyLangFields("fr", "en"));
document.querySelectorAll("[data-preview]").forEach((b) =>
b.addEventListener("click", () => togglePreview(b.dataset.preview))
);
initAuth().catch(() => {
$("login").hidden = false;
setMsg("Erreur réseau", "err");
});
renderLogExclude();