"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 = "" + blocked .map( ([uri, n]) => `` ) .join("") + "
CheminHits
${esc(uri)}${n.toLocaleString("fr-FR")}
"; } 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 = "" + by429 .map( ([ip, n]) => `` ) .join("") + "
IPLimitations
${esc(ip)}${n.toLocaleString("fr-FR")}
"; } } 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 ? `${rects}${labels}` : '

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]) => `
${esc(v)}${esc(label)}
` ) .join(""); if (!s.procs || s.procs.length === 0) { $("procs").innerHTML = '

Aucun processus.

'; } else { $("procs").innerHTML = "" + s.procs .sort((a, b) => (b.rss || 0) - (a.rss || 0)) .map( (p) => `` ) .join("") + "
NomPIDRAMCommande
${esc(p.name)}${p.pid}${fmtBytes(p.rss)}${esc(p.cmd)}
"; } if (!s.disk) { $("disk").innerHTML = '

Indisponible.

'; } else { const rows = s.disk.slice(1).map((line) => line.trim().split(/\s+/)); $("disk").innerHTML = "" + rows .map( (c) => `` ) .join("") + "
FichierTailleUtiliséDisponible%Monté sur
${esc(c[0])}${fmtBytes(Number(c[1]) * 1024)}${fmtBytes(Number(c[2]) * 1024)}${fmtBytes(Number(c[3]) * 1024)}${esc(c[4])}${esc(c[5])}
"; } } /* ---------- 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]) => `
${esc(v)}${esc(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) => `
${label}
${pct}%
`; $("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 = ''; 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 += ``; } else { gHtml += ``; } } gHtml += '
GrillePointsCellulesTaille gzipCouverture
${esc(label)}${g.points?.toLocaleString("fr-FR") ?? "—"}${g.cells?.toLocaleString("fr-FR") ?? "—"}${fmtBytes(g.totalSize)}${esc(cover)}
${esc(label)}pas encore construite
'; $("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 = "" + list .map( (a) => `` ) .join("") + "
SlugTitre FRTitre ENPubliéModifiéActions
${esc(a.slug)} ${esc(a.frTitle)} ${esc(a.enTitle)} ${esc(a.published)} ${esc(a.modified)}
"; } 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 = '' + ips .map((e) => { const badge = e.banned ? 'Bannie' : 'Active'; return ( "" + `` + `` + `` + `` + `` + `` + `` + "" ); }) .join("") + "
IPHitsCheminsPremierDernierStatut
${esc(e.ip)}${e.count}${hpEndpointsHtml(e.endpoints)}${fmtDate(Math.floor(e.first / 1000))}${fmtDate(Math.floor(e.last / 1000))}${badge}
"; } 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();