/* ===================================================================== aether-admin — Oberflächenkern Die Shell (Seitenleiste, Kopfleiste, Dialoge, Meldungen) und die einzige Brücke zum Server: `App.aufruf(modul, aktion, daten)`. Jede Sektion rendert aus echten Serverdaten — die Oberfläche hält keinen eigenen Spielzustand und entscheidet nichts. Rechte werden serverseitig geprüft; die UI blendet nur aus, was ohnehin abgelehnt würde. ===================================================================== */ "use strict"; const IST_FIVEM = (typeof GetParentResourceName === "function"); const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-admin"; const App = (() => { const zustand = { admin: { name: "—", gruppe: "user" }, sektionen: [], rechte: {}, media: { aktiv: false }, serverName: "Aether RP", sektion: "dashboard", }; const sektionen = {}; // [id] = renderfunktion const offeneRufe = {}; // [ruf] = { ok, fail } let rufZaehler = 0; let mapTimer = null; const $ = (id) => document.getElementById(id); /* ---- Helfer -------------------------------------------------------- */ function sicher(t) { if (t === null || t === undefined) return ""; return String(t).replace(/[&<>"']/g, (z) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[z])); } const euro = (n) => Math.round(n || 0).toLocaleString("de-DE") + " €"; const kompakt = (n) => n >= 1e6 ? (n / 1e6).toFixed(1) + " Mio" : n >= 1e3 ? (n / 1e3).toFixed(1) + "k" : "" + Math.round(n || 0); const initialen = (n) => String(n || "?").replace(/\s*\(.*\)/, "").split(" ") .map((w) => w[0]).slice(0, 2).join(""); function avColor(n) { let h = 0; for (const c of String(n || "")) h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 45% 45%)`; } function zeit(w) { if (!w) return "—"; const d = new Date(String(w).replace(" ", "T")); if (isNaN(d)) return sicher(w); return d.toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }); } function darf(schluessel) { return zustand.rechte[schluessel] === true; } /* ---- Server-Brücke ------------------------------------------------- */ function anClient(pfad, daten) { if (!IST_FIVEM) return Promise.resolve({}); return fetch(`https://${RESSOURCE}/${pfad}`, { method: "POST", headers: { "Content-Type": "application/json; charset=UTF-8" }, body: JSON.stringify(daten || {}), }).then((a) => a.json()).catch(() => ({})); } /** * Ruft eine Serveraktion auf und wartet auf die Antwort. * @returns {Promise} */ function aufruf(modul, aktion, daten) { return new Promise((ok, fail) => { if (!IST_FIVEM) { fail(new Error("Vorschaumodus — keine Serververbindung.")); return; } const ruf = ++rufZaehler; offeneRufe[ruf] = { ok, fail }; anClient("aufruf", { modul, aktion, daten: daten || {}, ruf }); setTimeout(() => { if (offeneRufe[ruf]) { delete offeneRufe[ruf]; fail(new Error("Zeitüberschreitung — der Server hat nicht geantwortet.")); } }, 15000); }); } /* ---- Meldungen & Dialoge ------------------------------------------- */ let toastTimer = null; function toast(text, typ) { const e = $("toast"); e.textContent = text; e.className = "toast show" + (typ ? " " + typ : ""); clearTimeout(toastTimer); toastTimer = setTimeout(() => e.classList.remove("show"), 2600); } function modal(html, klein) { $("overlay").innerHTML = `
${html}
`; $("overlay").classList.add("show"); } function closeModal() { $("overlay").classList.remove("show"); $("overlay").innerHTML = ""; } /* ---- Kopf & Navigation --------------------------------------------- */ function renderTop() { $("top").innerHTML = `
${sicher(zustand.serverName)}
${sicher(initialen(zustand.admin.name))}
${sicher(zustand.admin.name)}
${sicher(zustand.admin.gruppe)}
`; } function renderNav() { $("nav").innerHTML = zustand.sektionen.map((it) => it.grp ? `
${sicher(it.grp)}
` : ``).join(""); $("nav").querySelectorAll("[data-s]").forEach((b) => b.addEventListener("click", () => oeffneSektion(b.dataset.s))); } function oeffneSektion(id) { clearInterval(mapTimer); mapTimer = null; zustand.sektion = id; renderNav(); const rendern = sektionen[id] || sektionen.dashboard; $("main").innerHTML = `
⏳
Lädt …
`; Promise.resolve() .then(() => rendern($("main"))) .catch((f) => { $("main").innerHTML = `

Fehler

${sicher(f.message)}

`; }); } /* ---- Öffnen / Nachrichten vom Client ------------------------------- */ function zeigePanel(daten) { zustand.admin = daten.admin || zustand.admin; zustand.sektionen = daten.sektionen || []; zustand.rechte = daten.rechte || {}; zustand.media = daten.media || { aktiv: false }; zustand.serverName = daten.serverName || "Aether RP"; $("panel").hidden = false; document.body.style.display = ""; renderTop(); renderNav(); // Erste sichtbare Sektion öffnen const erste = zustand.sektionen.find((s) => s.id); oeffneSektion(erste ? erste.id : "dashboard"); } function schliessePanel() { $("panel").hidden = true; document.body.style.display = "none"; closeModal(); clearInterval(mapTimer); mapTimer = null; } window.addEventListener("message", (e) => { const d = e.data || {}; switch (d.aktion) { case "oeffnen": zeigePanel(d); break; case "schliessen": schliessePanel(); break; case "antwort": { const offen = offeneRufe[d.ruf]; if (!offen) break; delete offeneRufe[d.ruf]; if (d.fehler) offen.fail(new Error(d.fehler)); else offen.ok(d.ergebnis || {}); break; } } }); document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; if ($("overlay").classList.contains("show")) { closeModal(); return; } schliessen(); }); function schliessen() { anClient("schliessen", {}); schliessePanel(); } /* ---- Öffentliche Schnittstelle ------------------------------------- */ return { registriere(id, fn) { sektionen[id] = fn; }, zustand, aufruf, anClient, toast, modal, closeModal, schliessen, sicher, euro, kompakt, initialen, avColor, zeit, darf, renderNav, oeffneSektion, $, setMapTimer(t) { clearInterval(mapTimer); mapTimer = t; }, start() { if (IST_FIVEM) document.body.style.display = "none"; else { // Eigenständige Browser-Vorschau: nur der Rahmen, keine Daten $("panel").hidden = false; zustand.sektionen = [{ grp: "Vorschau" }, { id: "dashboard", label: "Dashboard", icon: "📊" }]; renderTop(); renderNav(); $("main").innerHTML = `
🖥️
Vorschaumodus — im Spiel mit F9 öffnen. Ohne Server keine Live-Daten.
`; } }, }; })(); App.start();