/* =====================================================================
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") ? '📢 Ankündigung ' : ""}
${darf("welt.wetter") ? '🌦️ Weltsteuerung ' : ""}
${darf("ressourcen.neustart") ? '⟳ Neustart ankündigen ' : ""}
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.
Countdown (Minuten)
Grund (optional)
Abbrechen Ankündigen
`, 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`,
`
Alle
Auffällig
`) +
`🔎
ID Spieler Job / Fraktion Region Ping Geld Status
`;
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)}
Verwalten `).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") ? '
📹 Live-KameraWebRTC ' : ""}
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("")}` : ""}
Schließen
`);
$("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,
`Konto Bank Bargeld
Betrag (negativ = entfernen) `,
() => A("spieler", "geld", { id: p.id, konto: $("x-konto").value, betrag: +$("x-betrag").value }), p);
if (a === "item") return eingabeModal("Item geben · " + p.name,
`Item
Menge `,
() => 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,
`Modell (Spawnname) `,
() => A("spieler", "fahrzeug", { id: p.id, modell: $("x-modell").value.trim() }), p);
if (a === "job") return eingabeModal("Job / Fraktion · " + p.name,
`Job ${(d.jobs || []).map((j) => `${sicher(j)} `).join("")}
Fraktion (leer = keine) `,
() => 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,
`Grund `,
() => 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,
`Grund `,
() => A("spieler", "kick", { id: p.id, grund: $("x-grund").value.trim() }), null, "red");
if (a === "ban") return eingabeModal("Bannen · " + p.name,
`Grund
Dauer 1 Tag 3 Tage 7 Tage 30 Tage permanent `,
() => 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}
Abbrechen Ausführen
`, 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`) +
`
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") ? '🧹 Leere aufräumen ' : ""}${darf("fahrzeuge.spawn") ? '+ Spawnen ' : ""}
`) +
`Kennzeichen Position Status
${d.fahrzeuge.map((v) => `${sicher(v.plate || "—")}
${v.x.toFixed(0)}, ${v.y.toFixed(0)}
${v.leer ? 'leer ' : 'besetzt '}
${darf("fahrzeuge.reparieren") ? `Reparieren ` : ""}${darf("fahrzeuge.herholen") ? ` Herholen ` : ""}${darf("fahrzeuge.loeschen") ? ` Löschen ` : ""} `).join("") || '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 Für Spieler — du selbst — ${(main._spieler || []).map((s) => `${sicher(s.name)} (#${s.id}) `).join("")}
Modell
Abbrechen 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") +
`
🌦️ Wetter
${wetter.map((w) => `${w} `).join("")}
🌑 Blackout umschalten
🧹 Bereinigung
Leere Fahrzeuge NPCs Objekte Leichen
`;
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") +
`
Typ Info Warnung Event
Nachricht
📢 Senden
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") ? '+ Manueller Ban ' : "") +
`
🔨 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") ? `
Entbannen ` : ""}
`).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") ? `
Löschen ` : ""}
`).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 Name oder Lizenz Grund Dauer 1 Tag 7 Tage 30 Tage permanent Abbrechen Bannen
`, 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" ? `Öffnen ` : `${darf("reports.claim") ? `Übernehmen ` : ""}${darf("reports.antwort") ? `Antworten ` : ""}Schließen `}
`;
}).join("") || '
'}
`;
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)} Abbrechen Senden
`, 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)}
${darf("wirtschaft.geldAlle") ? `Aktionen
💸 Geld an alle 🏛️ Staatskasse 📊 Steuersatz
` : ""}`;
const dialog = (titel, feld, aktion) => { modal(`${titel} ${feld}Abbrechen Speichern
`, 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", `Betrag pro Spieler (Bank) `, () => A("wirtschaft", "geldAlle", { betrag: +$("wx").value })));
const eK = $("e-kasse"); if (eK) eK.addEventListener("click", () => dialog("Staatskasse", `Neuer Stand `, () => A("wirtschaft", "kasse", { wert: +$("wx").value })));
const eS = $("e-steuer"); if (eS) eS.addEventListener("click", () => dialog("Steuersatz", `Prozent `, () => 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") ? '+ Neue ' : "") +
`${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") ? `Bearbeiten ` : ""}${darf("fraktionen.payday") && f.gehalt ? `Payday ` : ""}${darf("fraktionen.loeschen") ? `Löschen ` : ""}
`).join("") || '
Keine Fraktionen.
'}
`;
main._fr = d.fraktionen;
const frModal = (f) => { modal(`${f ? "Fraktion bearbeiten" : "Neue Fraktion"} Name Gehalt (€/h) Farbe Abbrechen Speichern
`, 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`) +
`Objekt Lage Besitzer Wert
${d.immobilien.map((im) => `🏠 ${sicher(im.name)} ${sicher(im.ort || "—")} ${im.besitzerName ? sicher(im.besitzerName) : 'frei '} ${euro(im.preis)}
${darf("immobilien.zuweisen") ? `Besitzer ` : ""}${im.besitzer && darf("immobilien.raeumen") ? ` Räumen ` : ""} `).join("") || '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 Online-Charakter — frei — ${(d.spieler || []).map((s) => `${sicher(s.name)} `).join("")}Abbrechen 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 = `Abstimmung beenden `;
else if (!p.aktiviert && !p.offen && positiv && darf("staat.aktivieren")) aktion = `Aktivieren `;
else if (!p.aktiviert && !p.offen && !positiv && darf("staat.archivieren")) aktion = `Archivieren `;
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") ? '⟳ Neustart ' : "") +
`Ressource Status Version
${d.ressourcen.map((r, i) => `🧩 ${sicher(r.name)} ${r.status === "started" ? 'läuft ' : '' + sicher(r.status) + " "} ${sicher(r.version)}
${darf("ressourcen.steuern") ? (r.status === "started" ? `Restart Stop ` : `Start `) : ""} `).join("")}
`;
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`) +
`
Zeit Admin Aktion Ziel Detail
${d.eintraege.map((l) => `${zeit(l.erstellt_am)} ${sicher(l.admin_name)} ${sicher(l.aktion)} ${sicher(l.ziel || "")} ${sicher(l.detail || "")} `).join("") || '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") ? '+ Rolle vergeben ' : "") +
`${d.team.map((a) => `
${sicher(initialen(a.name))}
${sicher(a.name)}
${sicher(a.admin_group)}
${a.online ? "online" : "offline"}
${darf("team.entfernen") ? `
Rolle ändern 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 ? "" : 'Name oder Lizenz '}Rolle ${main._rollen.map((r) => `${r} `).join("")} Abbrechen Speichern
`, 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) => `${d.schalter[k] ? "AN" : "AUS"} `;
main.innerHTML = kopf("Einstellungen", "Server-Konfiguration") +
`
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"); } });
});
})();