/* ===================================================================== aether-admin — Sektionen Jede Sektion lädt ihre Daten frisch vom Server und ruft für Eingriffe `App.aufruf(...)`. Kein lokaler Spielzustand — was hier steht, kommt vom Server, und was hier gedrückt wird, prüft der Server erneut. ===================================================================== */ "use strict"; (() => { const { sicher, euro, kompakt, initialen, avColor, zeit, darf } = App; const A = (m, a, d) => App.aufruf(m, a, d); const neu = () => App.oeffneSektion(App.zustand.sektion); const toast = App.toast, modal = App.modal, closeModal = App.closeModal; const $ = App.$; const pingPill = (p) => `${p} ms`; function flags(p) { let f = ""; if (p.god) f += 'GOD'; if (p.frozen) f += 'FROZEN'; if (p.muted) f += 'MUTE'; if (p.warns > 0) f += `${p.warns}⚠`; if (p.istTot) f += '†'; return f || '—'; } const kopf = (titel, unter, rechts) => `

${sicher(titel)}

${unter || ""}

${rechts || ""}
`; /* ===== Flächen-Diagramm ============================================= */ function areaChart(werte, W, H, farbe) { if (!werte || werte.length < 2) werte = [0, 0]; const min = Math.min(...werte), max = Math.max(...werte), span = (max - min) || 1; const x = (i) => 6 + (W - 12) * (i / (werte.length - 1)); const y = (v) => 6 + (H - 12) * (1 - (v - min) / span); let d = "M" + x(0).toFixed(1) + " " + y(werte[0]).toFixed(1); for (let i = 1; i < werte.length; i++) d += " L" + x(i).toFixed(1) + " " + y(werte[i]).toFixed(1); const uid = "g" + Math.random().toString(36).slice(2, 6); return ` `; } /* ===== DASHBOARD ==================================================== */ App.registriere("dashboard", async (main) => { const d = await A("dashboard", "laden"); if ($("top-live")) $("top-live").innerHTML = `Spieler ${d.online}/${d.slots} Reports ${d.offeneReports} Ressourcen ${d.ressourcen.laufend}/${d.ressourcen.gesamt}`; main.innerHTML = kopf("Dashboard", "Serverüberblick in Echtzeit") + `
👥 Spieler online
${d.online}/${d.slots}
${d.auslastung}% Auslastung
🎫 Offene Reports
${d.offeneReports}
${d.hoherPing} mit hohem Ping
🔨 Aktive Bans
${d.aktiveBans}
durchgesetzt
🧩 Ressourcen
${d.ressourcen.laufend}/${d.ressourcen.gesamt}
laufen
Spieler online · Verlauf jetzt ${d.online}
${areaChart(d.verlauf, 600, 150, "#5b8cff")}
Schnellaktionen
${darf("broadcast.senden") ? '' : ""} ${darf("welt.wetter") ? '' : ""} ${darf("ressourcen.neustart") ? '' : ""}
Letzte Admin-Aktionen
${(d.letzteLogs || []).map((l) => `
${sicher(l.admin_name)}: ${sicher(l.aktion)}${l.ziel ? " · " + sicher(l.ziel) : ""}${zeit(l.erstellt_am)}
`).join("") || '
Noch nichts protokolliert.
'}
`; main.querySelectorAll("[data-q]").forEach((b) => b.addEventListener("click", () => { if (b.dataset.q === "restart") return neustartModal(); App.oeffneSektion(b.dataset.q); })); }); function neustartModal() { modal(`

Server-Neustart ankündigen

Alle Spieler werden benachrichtigt.
`, true); $("rs-c").addEventListener("click", closeModal); $("rs-ok").addEventListener("click", async () => { try { const r = await A("ressourcen", "neustart", { minuten: +$("rs-min").value, grund: $("rs-grund").value.trim() }); closeModal(); toast(r.hinweis || "Angekündigt", "warn"); } catch (f) { toast(f.message, "warn"); } }); } /* ===== SPIELER ===================================================== */ let spielerSuche = "", spielerFilter = "alle"; App.registriere("spieler", async (main) => { const d = await A("spieler", "liste"); main._spieler = d.spieler; main._jobs = d.jobs; main.innerHTML = kopf("Spieler", `${d.spieler.length} online · Verwaltung & Aktionen`, `
`) + `
IDSpielerJob / FraktionRegionPingGeldStatus
`; const s = $("sp-such"); s.addEventListener("input", () => { spielerSuche = s.value.trim().toLowerCase(); zeichne(); }); main.querySelectorAll("#sp-filter [data-f]").forEach((b) => b.addEventListener("click", () => { spielerFilter = b.dataset.f; neu(); })); zeichne(); function liste() { return main._spieler.filter((p) => { const q = !spielerSuche || (p.name + " " + p.region).toLowerCase().includes(spielerSuche); const f = spielerFilter === "alle" || (p.warns > 0 || p.ping >= 120 || p.muted || p.istTot); return q && f; }); } function zeichne() { const el = $("sp-body"); if (!el) return; el.innerHTML = liste().map((p) => ` #${p.id} ${sicher(initialen(p.name))}${sicher(p.name)} ${sicher(p.job)}${p.fraktion ? ` ${sicher(p.fraktion)}` : ""} ${sicher(p.region)}${pingPill(p.ping)} ${euro(p.cash + p.bank)}${flags(p)} `).join("") || 'Keine Spieler.'; el.querySelectorAll("[data-p]").forEach((b) => b.addEventListener("click", () => spielerModal(+b.dataset.p))); } }); async function spielerModal(id) { let d; try { d = await A("spieler", "details", { id }); } catch (f) { return toast(f.message, "warn"); } const p = d.spieler; const knopf = (a, icon, label, sub, extra) => darf("spieler." + a) ? `` : ""; modal(`

${sicher(initialen(p.name))} ${sicher(p.name)} #${p.id}

${sicher(p.job)}${p.fraktion ? " · " + sicher(p.fraktion) : ""} · ${sicher(p.region)} · ${d.spielzeit || 0} Min · Gruppe ${sicher(d.gruppe)}
Bargeld${euro(p.cash)}
Bank${euro(p.bank)}
Ping${p.ping} ms
Leben${p.leben}
Verwarnungen${p.warns}
Godmode${p.god ? "an" : "aus"}
Teleport & Ansicht
${knopf("goto", "🧭", "Zu Spieler", "Teleport hin")}${knopf("bring", "📥", "Herholen", "zu mir")} ${knopf("spectate", "👁️", "Beobachten", "native")}${darf("kamera.oeffnen") ? '' : ""}
Zustand
${knopf("heal", "❤️", "Heilen", "volle HP")}${knopf("revive", "✨", "Wiederbeleben", "revive")}${knopf("armor", "🛡️", "Rüstung", "+Armor")} ${knopf("freeze", "🧊", "Einfrieren", p.frozen ? "aktiv" : "toggle", p.frozen ? "on" : "")} ${knopf("god", "⭐", "Godmode", p.god ? "aktiv" : "toggle", p.god ? "on" : "")} ${knopf("mute", "🔇", "Stumm", p.muted ? "aktiv" : "toggle", p.muted ? "on" : "")}
Geben & Setzen
${knopf("geld", "💵", "Geld", "geben/nehmen")}${knopf("item", "🎁", "Item", "geben")}${knopf("fahrzeug", "🚗", "Fahrzeug", "spawnen")}${knopf("job", "💼", "Job/Fraktion", "setzen")}
Moderation
${knopf("warn", "⚠️", "Verwarnen", "Warn")}${knopf("notiz", "📝", "Notiz", "intern")} ${knopf("kick", "👢", "Kicken", "trennen", "danger")}${knopf("ban", "🔨", "Bannen", "sperren", "danger")}
${d.warns.length ? `
Verwarnungen
${d.warns.map((w) => `
${sicher(w.grund)}${sicher(w.admin_name)} · ${zeit(w.erstellt_am)}
`).join("")}` : ""} `); $("pm-close").addEventListener("click", closeModal); $("overlay").querySelectorAll("[data-a]").forEach((b) => b.addEventListener("click", () => spielerAktion(p, b.dataset.a, d))); } async function spielerAktion(p, a, d) { const einfach = { goto: 1, bring: 1, spectate: 1, heal: 1, revive: 1, armor: 1, freeze: 1, god: 1, mute: 1 }; if (einfach[a]) { try { await A("spieler", a, { id: p.id }); toast("Ausgeführt", "good"); spielerModal(p.id); neu(); } catch (f) { toast(f.message, "warn"); } return; } if (a === "kamera") { closeModal(); App.oeffneSektion("cams"); if (window.Kameras) window.Kameras.starte([p.id], p.name); return; } if (a === "geld") return eingabeModal("Geld · " + p.name, ` `, () => A("spieler", "geld", { id: p.id, konto: $("x-konto").value, betrag: +$("x-betrag").value }), p); if (a === "item") return eingabeModal("Item geben · " + p.name, ` `, () => A("spieler", "item", { id: p.id, item: $("x-item").value.trim(), menge: +$("x-menge").value }), p); if (a === "fahrzeug") return eingabeModal("Fahrzeug spawnen · " + p.name, ``, () => A("spieler", "fahrzeug", { id: p.id, modell: $("x-modell").value.trim() }), p); if (a === "job") return eingabeModal("Job / Fraktion · " + p.name, ` `, () => A("spieler", "job", { id: p.id, charakterId: p.charakterId, job: $("x-job").value, fraktion: $("x-frak").value.trim() }), p); if (a === "warn") return eingabeModal("Verwarnen · " + p.name, ``, () => A("spieler", "warn", { id: p.id, grund: $("x-grund").value.trim() }), p); if (a === "notiz") return eingabeModal("Interne Notiz · " + p.name, ``, () => A("spieler", "notiz", { id: p.id, text: $("x-text").value.trim() }), p); if (a === "kick") return eingabeModal("Kicken · " + p.name, ``, () => A("spieler", "kick", { id: p.id, grund: $("x-grund").value.trim() }), null, "red"); if (a === "ban") return eingabeModal("Bannen · " + p.name, ` `, () => A("spieler", "ban", { id: p.id, grund: $("x-grund").value.trim(), dauer: $("x-dauer").value }), null, "red"); } /** Kleiner Eingabedialog; `zielRefresh` = Spieler neu öffnen, sonst nur Liste */ function eingabeModal(titel, felder, aktion, zielRefresh, farbe) { modal(`

${sicher(titel)}

${felder} `, true); $("e-c").addEventListener("click", () => zielRefresh ? spielerModal(zielRefresh.id) : closeModal()); $("e-ok").addEventListener("click", async () => { try { await aktion(); closeModal(); toast("Ausgeführt", "good"); if (zielRefresh) spielerModal(zielRefresh.id); neu(); } catch (f) { toast(f.message, "warn"); } }); } /* ===== WELTKARTE =================================================== */ const frakFarbe = (f) => ({ police: "#4aa8ff", ambulance: "#f0506e" }[f] || "#34d39a"); App.registriere("weltkarte", async (main) => { const d = await A("weltkarte", "laden"); main.innerHTML = kopf("Weltkarte", `${d.spieler.length} Spieler live · Marker anklicken`) + `
Vinewood
Downtown
Vespucci
Spieler ${d.spieler.length}
`; zeichneMarker(d.spieler); App.setMapTimer(setInterval(async () => { try { const r = await A("weltkarte", "laden"); zeichneMarker(r.spieler); } catch (e) {} }, 4000)); function zeichneMarker(sp) { // GTA-Weltkoordinaten grob auf 0..100% abbilden const zx = (x) => Math.max(2, Math.min(98, (x + 4000) / 8000 * 100)); const zy = (y) => Math.max(2, Math.min(98, (4000 - y) / 8000 * 100)); const mk = $("marker"); if (!mk) return; mk.innerHTML = sp.map((p) => `
${sicher(initialen(p.name))}
${sicher(p.name.split(" ")[0])}
`).join(""); const ml = $("mapliste"); if (ml) ml.innerHTML = sp.map((p) => `
${sicher(initialen(p.name))}
${sicher(p.name)}
${p.ping} ms
`).join(""); mk.querySelectorAll("[data-mk]").forEach((m) => m.addEventListener("click", () => spielerModal(+m.dataset.mk))); if (ml) ml.querySelectorAll("[data-pq]").forEach((r) => r.addEventListener("click", () => spielerModal(+r.dataset.pq))); } }); /* ===== FAHRZEUGE ================================================== */ App.registriere("fahrzeuge", async (main) => { const d = await A("fahrzeuge", "laden"); main.innerHTML = kopf("Fahrzeuge", `${d.anzahl} in der Welt`, `
${darf("fahrzeuge.aufraeumen") ? '' : ""}${darf("fahrzeuge.spawn") ? '' : ""}
`) + `
${d.fahrzeuge.map((v) => ``).join("") || ''}
KennzeichenPositionStatus
${sicher(v.plate || "—")} ${v.x.toFixed(0)}, ${v.y.toFixed(0)} ${v.leer ? 'leer' : 'besetzt'} ${darf("fahrzeuge.reparieren") ? `` : ""}${darf("fahrzeuge.herholen") ? ` ` : ""}${darf("fahrzeuge.loeschen") ? ` ` : ""}
Keine Fahrzeuge.
`; main._spieler = d.spieler; const bind = (sel, m, a, extra) => main.querySelectorAll(sel).forEach((b) => b.addEventListener("click", async () => { try { const r = await A("fahrzeuge", a, Object.assign({ netId: +b.dataset[extra] }, {})); toast(r.entfernt !== undefined ? r.entfernt + " entfernt" : "Erledigt", "good"); neu(); } catch (f) { toast(f.message, "warn"); } })); bind("[data-fix]", "fahrzeuge", "reparieren", "fix"); bind("[data-her]", "fahrzeuge", "herholen", "her"); bind("[data-del]", "fahrzeuge", "loeschen", "del"); const cl = $("fz-clean"); if (cl) cl.addEventListener("click", async () => { try { const r = await A("fahrzeuge", "aufraeumen"); toast(r.entfernt + " entfernt", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); const sp = $("fz-spawn"); if (sp) sp.addEventListener("click", () => { modal(`

Fahrzeug spawnen

`, true); $("fs-c").addEventListener("click", closeModal); $("fs-ok").addEventListener("click", async () => { try { await A("fahrzeuge", "spawn", { ziel: $("fs-ziel").value ? +$("fs-ziel").value : null, modell: $("fs-modell").value.trim() }); closeModal(); toast("Gespawnt", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); }); }); /* ===== WELTSTEUERUNG ============================================== */ App.registriere("welt", async (main) => { const wetter = ["Klar", "Bewölkt", "Regen", "Gewitter", "Nebel", "Schnee", "Sturm"]; main.innerHTML = kopf("Weltsteuerung", "Zeit, Wetter & Umgebung") + `
🕒 Zeit
🌦️ Wetter
${wetter.map((w) => ``).join("")}
🧹 Bereinigung
`; const setze = async (m, a, dd, txt) => { try { await A(m, a, dd); toast(txt, "good"); } catch (f) { toast(f.message, "warn"); } }; main.querySelectorAll("[data-h]").forEach((b) => b.addEventListener("click", () => { $("w-stunde").value = b.dataset.h; setze("welt", "zeit", { stunde: +b.dataset.h }, "Zeit " + b.dataset.h + ":00"); })); $("w-zeitfix").addEventListener("click", () => setze("welt", "zeit", { stunde: +$("w-stunde").value, fix: true }, "Zeit eingefroren")); main.querySelectorAll("[data-w]").forEach((b) => b.addEventListener("click", () => setze("welt", "wetter", { wetter: b.dataset.w }, "Wetter: " + b.dataset.w))); $("w-blackout").addEventListener("click", () => setze("welt", "blackout", {}, "Blackout umgeschaltet")); main.querySelectorAll("[data-clean]").forEach((b) => b.addEventListener("click", async () => { try { const r = await A("welt", "cleanup", { art: b.dataset.clean }); toast(r.entfernt + " entfernt", "good"); } catch (f) { toast(f.message, "warn"); } })); }); /* ===== ANKÜNDIGUNGEN ============================================= */ App.registriere("broadcast", async (main) => { const d = await A("broadcast", "laden"); main.innerHTML = kopf("Ankündigungen", "Nachricht an alle Spieler") + `
Verlauf
${(d.verlauf || []).map((b) => `
${sicher(b.typ)} ${sicher(b.nachricht)}${zeit(b.erstellt_am)}
`).join("") || '
Noch keine.
'}
`; $("bc-send").addEventListener("click", async () => { const m = $("bc-msg").value.trim(); if (!m) return toast("Nachricht eingeben", "warn"); try { await A("broadcast", "senden", { typ: $("bc-typ").value, nachricht: m }); toast("Gesendet", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); }); /* ===== MODERATION =============================================== */ App.registriere("moderation", async (main) => { const d = await A("moderation", "laden"); main.innerHTML = kopf("Bans & Verwarnungen", `${d.bans.length} Bans · ${d.warns.length} Warns`, darf("moderation.banManuell") ? '' : "") + `
🔨 Aktive Bans
${d.bans.map((b) => `
${sicher(b.name)}
${sicher(b.grund)} · ${b.bis ? "bis " + zeit(b.bis) : "permanent"} · ${sicher(b.admin_name)}
${darf("moderation.unban") ? `` : ""}
`).join("") || '
Keine Bans.
'}
⚠️ Verwarnungen
${d.warns.map((w) => `
${sicher(w.name)}
${sicher(w.grund)} · ${sicher(w.admin_name)} · ${zeit(w.erstellt_am)}
${darf("moderation.warnWeg") ? `` : ""}
`).join("") || '
Keine Verwarnungen.
'}
`; main.querySelectorAll("[data-unban]").forEach((b) => b.addEventListener("click", async () => { try { await A("moderation", "unban", { id: +b.dataset.unban }); toast("Entbannt", "good"); neu(); } catch (f) { toast(f.message, "warn"); } })); main.querySelectorAll("[data-warn]").forEach((b) => b.addEventListener("click", async () => { try { await A("moderation", "warnWeg", { id: +b.dataset.warn }); toast("Gelöscht", "good"); neu(); } catch (f) { toast(f.message, "warn"); } })); const mb = $("mod-ban"); if (mb) mb.addEventListener("click", () => { modal(`

Manueller Ban

`, true); $("mb-c").addEventListener("click", closeModal); $("mb-ok").addEventListener("click", async () => { const n = $("mb-name").value.trim(); if (!n) return toast("Name angeben", "warn"); try { await A("moderation", "banManuell", { name: n, grund: $("mb-grund").value.trim(), dauer: $("mb-dauer").value }); closeModal(); toast("Gebannt", "warn"); neu(); } catch (f) { toast(f.message, "warn"); } }); }); }); /* ===== REPORTS ================================================== */ App.registriere("reports", async (main) => { const d = await A("reports", "laden"); const prioP = { 1: ["Prio 1", "r"], 2: ["Prio 2", "a"], 3: ["Prio 3", "b"] }; const stP = { offen: ["offen", "a"], bearbeitung: ["in Bearbeitung", "b"], geschlossen: ["geschlossen", "g"] }; main.innerHTML = kopf("Reports / Support", `${d.reports.filter((r) => r.status !== "geschlossen").length} offen · ${d.reports.length} gesamt`) + `
${d.reports.map((r) => { const pp = prioP[r.prioritaet] || ["?", "m"], sp = stP[r.status] || ["?", "m"]; return `
${sicher(r.betreff)}
${pp[0]}${sp[0]}von ${sicher(r.melder_name)}${r.bearbeiter ? " · " + sicher(r.bearbeiter) : ""}
${r.status === "geschlossen" ? `` : `${darf("reports.claim") ? `` : ""}${darf("reports.antwort") ? `` : ""}`}
`; }).join("") || '
🎫
Keine Reports.
'}
`; main.querySelectorAll("[data-claim]").forEach((b) => b.addEventListener("click", async () => { try { await A("reports", "claim", { id: +b.dataset.claim }); toast("Übernommen", "good"); neu(); } catch (f) { toast(f.message, "warn"); } })); main.querySelectorAll("[data-st]").forEach((b) => b.addEventListener("click", async () => { const [id, st] = b.dataset.st.split(":"); try { await A("reports", "status", { id: +id, status: st }); toast("Aktualisiert", "good"); neu(); } catch (f) { toast(f.message, "warn"); } })); main.querySelectorAll("[data-ans]").forEach((b) => b.addEventListener("click", () => { const [id, name] = b.dataset.ans.split(":"); modal(`

Antwort · ${sicher(name)}

`, true); $("ra-c").addEventListener("click", closeModal); $("ra-ok").addEventListener("click", async () => { const t = $("ra").value.trim(); if (!t) return toast("Text eingeben", "warn"); try { await A("reports", "antwort", { id: +id, text: t }); closeModal(); toast("Gesendet", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); })); }); /* ===== WIRTSCHAFT =============================================== */ App.registriere("wirtschaft", async (main) => { const d = await A("wirtschaft", "laden"); main.innerHTML = kopf("Wirtschaft", "Geldmenge, Staatskasse & Steuern") + `
💶 Geld im Umlauf
${kompakt(d.umlauf)}
${d.charaktere} Charaktere
🏛️ Staatskasse
${kompakt(d.staatskasse)}
👤 Ø Vermögen
${kompakt(d.charaktere ? d.umlauf / d.charaktere : 0)}
📊 Steuersatz
${d.steuer}%
${darf("wirtschaft.geldAlle") ? `
Aktionen
` : ""}`; const dialog = (titel, feld, aktion) => { modal(`

${titel}

${feld}`, true); $("w-c").addEventListener("click", closeModal); $("w-ok").addEventListener("click", async () => { try { await aktion(); closeModal(); toast("Gespeichert", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); }; const eAll = $("e-all"); if (eAll) eAll.addEventListener("click", () => dialog("Geld an alle", ``, () => A("wirtschaft", "geldAlle", { betrag: +$("wx").value }))); const eK = $("e-kasse"); if (eK) eK.addEventListener("click", () => dialog("Staatskasse", ``, () => A("wirtschaft", "kasse", { wert: +$("wx").value }))); const eS = $("e-steuer"); if (eS) eS.addEventListener("click", () => dialog("Steuersatz", ``, () => A("wirtschaft", "steuer", { wert: +$("wx").value }))); }); /* ===== FRAKTIONEN ============================================== */ App.registriere("fraktionen", async (main) => { const d = await A("fraktionen", "laden"); main.innerHTML = kopf("Fraktionen", `${d.fraktionen.length} Fraktionen`, darf("fraktionen.speichern") ? '' : "") + `
${d.fraktionen.map((f) => `
${sicher(initialen(f.name))}
${sicher(f.name)}
${f.mitglieder} Mitglieder · ${f.online} online
${f.gehalt ? euro(f.gehalt) + " /h" : "kein Payday"}
${darf("fraktionen.speichern") ? `` : ""}${darf("fraktionen.payday") && f.gehalt ? `` : ""}${darf("fraktionen.loeschen") ? `` : ""}
`).join("") || '
Keine Fraktionen.
'}
`; main._fr = d.fraktionen; const frModal = (f) => { modal(`

${f ? "Fraktion bearbeiten" : "Neue Fraktion"}

`, true); $("f-c").addEventListener("click", closeModal); $("f-ok").addEventListener("click", async () => { const n = $("fn").value.trim(); if (!n) return toast("Name angeben", "warn"); try { await A("fraktionen", "speichern", { name: n, gehalt: +$("fg").value, farbe: $("ff").value.trim() }); closeModal(); toast("Gespeichert", "good"); neu(); } catch (e) { toast(e.message, "warn"); } }); }; const frn = $("fr-neu"); if (frn) frn.addEventListener("click", () => frModal(null)); main.querySelectorAll("[data-edit]").forEach((b) => b.addEventListener("click", () => frModal(main._fr.find((x) => x.name === b.dataset.edit)))); main.querySelectorAll("[data-pay]").forEach((b) => b.addEventListener("click", async () => { try { const r = await A("fraktionen", "payday", { name: b.dataset.pay }); toast(r.anzahl + " ausgezahlt", "good"); } catch (f) { toast(f.message, "warn"); } })); main.querySelectorAll("[data-del]").forEach((b) => b.addEventListener("click", async () => { try { await A("fraktionen", "loeschen", { name: b.dataset.del }); toast("Gelöscht", "warn"); neu(); } catch (f) { toast(f.message, "warn"); } })); }); /* ===== IMMOBILIEN ============================================= */ App.registriere("immobilien", async (main) => { const d = await A("immobilien", "laden"); main.innerHTML = kopf("Immobilien", `${d.immobilien.length} Objekte`) + `
${d.immobilien.map((im) => ``).join("") || ''}
ObjektLageBesitzerWert
🏠 ${sicher(im.name)}${sicher(im.ort || "—")}${im.besitzerName ? sicher(im.besitzerName) : 'frei'}${euro(im.preis)} ${darf("immobilien.zuweisen") ? `` : ""}${im.besitzer && darf("immobilien.raeumen") ? ` ` : ""}
Keine Objekte.
`; main.querySelectorAll("[data-evict]").forEach((b) => b.addEventListener("click", async () => { try { await A("immobilien", "raeumen", { id: +b.dataset.evict }); toast("Geräumt", "warn"); neu(); } catch (f) { toast(f.message, "warn"); } })); main.querySelectorAll("[data-assign]").forEach((b) => b.addEventListener("click", () => { modal(`

Besitzer setzen

`, true); $("im-c").addEventListener("click", closeModal); $("im-ok").addEventListener("click", async () => { try { await A("immobilien", "zuweisen", { id: +b.dataset.assign, besitzer: $("im-o").value ? +$("im-o").value : null }); closeModal(); toast("Aktualisiert", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); })); }); /* ===== STAATSPROJEKTE ======================================== */ App.registriere("staat", async (main) => { const d = await A("staat", "laden"); main.innerHTML = kopf("Staatsprojekte", `Abstimmungen · Staatskasse ${euro(d.staatskasse)}`) + d.projekte.map((p) => { const positiv = p.zustimmung >= d.zustimmungMin; let status = p.aktiviert ? '✔ Umgesetzt' : p.offen ? 'Abstimmung läuft' : positiv ? 'bereit' : 'abgelehnt'; let aktion = ""; if (!p.aktiviert && p.offen && darf("staat.beenden")) aktion = ``; else if (!p.aktiviert && !p.offen && positiv && darf("staat.aktivieren")) aktion = ``; else if (!p.aktiviert && !p.offen && !positiv && darf("staat.archivieren")) aktion = ``; return `
${sicher(p.titel)} ${status}
${sicher(p.beschreibung || "")} · Kosten: ${euro(p.kosten)}
${p.ja} Dafür · ${p.nein} Dagegen · ${p.zustimmung}% (nötig: ${d.zustimmungMin}%)
${aktion}
`; }).join("") || '
Keine Projekte.
'; const act = async (a, id) => { try { await A("staat", a, { id }); toast("Erledigt", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }; main.querySelectorAll("[data-end]").forEach((b) => b.addEventListener("click", () => act("beenden", +b.dataset.end))); main.querySelectorAll("[data-act]").forEach((b) => b.addEventListener("click", () => act("aktivieren", +b.dataset.act))); main.querySelectorAll("[data-arch]").forEach((b) => b.addEventListener("click", () => act("archivieren", +b.dataset.arch))); }); /* ===== RESSOURCEN ============================================ */ App.registriere("ressourcen", async (main) => { const d = await A("ressourcen", "laden"); main.innerHTML = kopf("Ressourcen", `${d.ressourcen.filter((r) => r.status === "started").length}/${d.ressourcen.length} aktiv`, darf("ressourcen.neustart") ? '' : "") + `
${d.ressourcen.map((r, i) => ``).join("")}
RessourceStatusVersion
🧩 ${sicher(r.name)}${r.status === "started" ? 'läuft' : '' + sicher(r.status) + ""}${sicher(r.version)} ${darf("ressourcen.steuern") ? (r.status === "started" ? ` ` : ``) : ""}
`; main.querySelectorAll("[data-r]").forEach((b) => b.addEventListener("click", async () => { const [name, aktion] = b.dataset.r.split(":"); try { await A("ressourcen", "steuern", { name, aktion }); toast(name + " " + aktion, "good"); neu(); } catch (f) { toast(f.message, "warn"); } })); const rr = $("rz-restart"); if (rr) rr.addEventListener("click", neustartModal); }); /* ===== AUDIT-LOG ============================================ */ let logSuche = "", logAdmin = "alle"; App.registriere("logs", async (main) => { const d = await A("logs", "laden", { suche: logSuche, admin: logAdmin === "alle" ? "" : logAdmin }); main.innerHTML = kopf("Audit-Log", `${d.eintraege.length} Einträge`) + `
${d.eintraege.map((l) => ``).join("") || ''}
ZeitAdminAktionZielDetail
${zeit(l.erstellt_am)}${sicher(l.admin_name)}${sicher(l.aktion)}${sicher(l.ziel || "")}${sicher(l.detail || "")}
Keine Einträge.
`; let t = null; $("lg-such").addEventListener("input", (e) => { clearTimeout(t); t = setTimeout(() => { logSuche = e.target.value.trim(); neu(); }, 300); }); $("lg-admin").addEventListener("change", (e) => { logAdmin = e.target.value; neu(); }); }); /* ===== ADMIN-TEAM ========================================== */ App.registriere("team", async (main) => { const d = await A("team", "laden"); const rolleP = { owner: "r", admin: "a", support: "b" }; main.innerHTML = kopf("Admin-Team", `${d.team.filter((a) => a.online).length}/${d.team.length} online`, darf("team.speichern") ? '' : "") + `
${d.team.map((a) => `
${sicher(initialen(a.name))}
${sicher(a.name)}
${sicher(a.admin_group)}
${a.online ? "online" : "offline"}
${darf("team.entfernen") ? `
` : ""}
`).join("") || '
Kein Team.
'}
Die Rolle steht in aether_users.admin_group — der Core respektiert sie überall. owner › admin › support.
`; main._rollen = d.rollen; const rollenModal = (lizenz, name) => { modal(`

Rolle · ${sicher(name || "neuer Benutzer")}

${lizenz ? "" : ''}`, true); $("tr-c").addEventListener("click", closeModal); $("tr-ok").addEventListener("click", async () => { try { await A("team", "speichern", { lizenz: lizenz || null, name: lizenz ? name : ($("tr-name") ? $("tr-name").value.trim() : ""), rolle: $("tr-rolle").value }); closeModal(); toast("Gespeichert", "good"); neu(); } catch (f) { toast(f.message, "warn"); } }); }; const tn = $("tm-neu"); if (tn) tn.addEventListener("click", () => rollenModal(null, null)); main.querySelectorAll("[data-role]").forEach((b) => b.addEventListener("click", () => { const [l, n] = b.dataset.role.split(":"); rollenModal(l, n); })); main.querySelectorAll("[data-del]").forEach((b) => b.addEventListener("click", async () => { const [l, n] = b.dataset.del.split(":"); try { await A("team", "entfernen", { lizenz: l, name: n }); toast("Entfernt", "warn"); neu(); } catch (f) { toast(f.message, "warn"); } })); }); /* ===== EINSTELLUNGEN ====================================== */ App.registriere("settings", async (main) => { const d = await A("settings", "laden"); const sw = (k, label) => `
${label}
`; main.innerHTML = kopf("Einstellungen", "Server-Konfiguration") + `
Allgemein
Regeln & Systeme
${sw("pvp", "PvP")}${sw("whitelist", "Whitelist")}${sw("voice", "Voice-Chat")}${sw("economy", "Wirtschaft")}
`; main._schalter = Object.assign({}, d.schalter); main.querySelectorAll("[data-set]").forEach((b) => b.addEventListener("click", () => { main._schalter[b.dataset.set] = !main._schalter[b.dataset.set]; b.className = "btn " + (main._schalter[b.dataset.set] ? "green" : "ghost") + " sm"; b.textContent = main._schalter[b.dataset.set] ? "AN" : "AUS"; })); $("st-save").addEventListener("click", async () => { try { await A("settings", "speichern", { servername: $("st-name").value.trim(), neustartStunden: +$("st-rs").value, schalter: main._schalter }); toast("Gespeichert", "good"); } catch (f) { toast(f.message, "warn"); } }); }); })();