aether-framework/resources/[aether]/aether-admin/html/js/sections.js
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

489 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
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) => `<span class="pill ${p < 60 ? "g" : p < 120 ? "a" : "r"}">${p} ms</span>`;
function flags(p) {
let f = "";
if (p.god) f += '<span class="pill v">GOD</span>';
if (p.frozen) f += '<span class="pill b">FROZEN</span>';
if (p.muted) f += '<span class="pill a">MUTE</span>';
if (p.warns > 0) f += `<span class="pill r">${p.warns}⚠</span>`;
if (p.istTot) f += '<span class="pill r">†</span>';
return f || '<span class="pill m">—</span>';
}
const kopf = (titel, unter, rechts) =>
`<div class="phead"><div><h2>${sicher(titel)}</h2><p>${unter || ""}</p></div>${rechts || ""}</div>`;
/* ===== 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 `<svg viewBox="0 0 ${W} ${H}" style="height:${H}px"><defs><linearGradient id="${uid}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${farbe}" stop-opacity="0.35"/><stop offset="100%" stop-color="${farbe}" stop-opacity="0"/></linearGradient></defs>
<path d="${d} L${x(werte.length - 1).toFixed(1)} ${H - 6} L${x(0).toFixed(1)} ${H - 6} Z" fill="url(#${uid})"/>
<path d="${d}" fill="none" stroke="${farbe}" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>`;
}
/* ===== DASHBOARD ==================================================== */
App.registriere("dashboard", async (main) => {
const d = await A("dashboard", "laden");
if ($("top-live")) $("top-live").innerHTML =
`<span>Spieler <b>${d.online}/${d.slots}</b></span>
<span>Reports <b>${d.offeneReports}</b></span>
<span>Ressourcen <b>${d.ressourcen.laufend}/${d.ressourcen.gesamt}</b></span>`;
main.innerHTML = kopf("Dashboard", "Serverüberblick in Echtzeit") +
`<div class="grid" style="grid-template-columns:repeat(4,1fr)">
<div class="card kpi"><div class="l">👥 Spieler online</div><div class="v">${d.online}/${d.slots}</div><div class="d" style="color:var(--green)">${d.auslastung}% Auslastung</div></div>
<div class="card kpi"><div class="l">🎫 Offene Reports</div><div class="v" style="color:${d.offeneReports ? "var(--amber)" : "var(--green)"}">${d.offeneReports}</div><div class="d" style="color:var(--muted)">${d.hoherPing} mit hohem Ping</div></div>
<div class="card kpi"><div class="l">🔨 Aktive Bans</div><div class="v">${d.aktiveBans}</div><div class="d" style="color:var(--muted)">durchgesetzt</div></div>
<div class="card kpi"><div class="l">🧩 Ressourcen</div><div class="v">${d.ressourcen.laufend}<span style="font-size:16px;color:var(--muted)">/${d.ressourcen.gesamt}</span></div><div class="d" style="color:var(--muted)">laufen</div></div>
</div>
<div class="grid" style="grid-template-columns:1.6fr 1fr;margin-top:12px">
<div class="card"><div class="list-h">Spieler online · Verlauf <span class="r">jetzt ${d.online}</span></div><div class="chart">${areaChart(d.verlauf, 600, 150, "#5b8cff")}</div></div>
<div class="card"><div class="list-h">Schnellaktionen</div><div class="quick">
${darf("broadcast.senden") ? '<button class="btn ghost" data-q="broadcast">📢 Ankündigung</button>' : ""}
${darf("welt.wetter") ? '<button class="btn ghost" data-q="welt">🌦️ Weltsteuerung</button>' : ""}
${darf("ressourcen.neustart") ? '<button class="btn red" data-q="restart">⟳ Neustart ankündigen</button>' : ""}
</div></div>
</div>
<div class="card" style="margin-top:12px"><div class="list-h">Letzte Admin-Aktionen</div>
${(d.letzteLogs || []).map((l) => `<div class="kv"><span>${sicher(l.admin_name)}: ${sicher(l.aktion)}${l.ziel ? " · " + sicher(l.ziel) : ""}</span><span style="color:var(--muted)">${zeit(l.erstellt_am)}</span></div>`).join("") || '<div class="empty">Noch nichts protokolliert.</div>'}
</div>`;
main.querySelectorAll("[data-q]").forEach((b) => b.addEventListener("click", () => {
if (b.dataset.q === "restart") return neustartModal();
App.oeffneSektion(b.dataset.q);
}));
});
function neustartModal() {
modal(`<h3>Server-Neustart ankündigen</h3><div class="msub">Alle Spieler werden benachrichtigt.</div>
<label class="flabel">Countdown (Minuten)</label><input class="input" id="rs-min" type="number" value="5">
<label class="flabel">Grund (optional)</label><input class="input" id="rs-grund" placeholder="z. B. Update">
<div class="modal-actions"><button class="btn ghost" id="rs-c">Abbrechen</button><button class="btn red" id="rs-ok">Ankündigen</button></div>`, 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`,
`<div class="seg" id="sp-filter">
<button data-f="alle" class="${spielerFilter === "alle" ? "active" : ""}">Alle</button>
<button data-f="flagged" class="${spielerFilter === "flagged" ? "active" : ""}">Auffällig</button></div>`) +
`<div class="searchbar" style="margin-bottom:12px"><span>🔎</span><input id="sp-such" placeholder="Name oder Region…" value="${sicher(spielerSuche)}"></div>
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>ID</th><th>Spieler</th><th>Job / Fraktion</th><th>Region</th><th>Ping</th><th class="num">Geld</th><th>Status</th><th></th></tr></thead><tbody id="sp-body"></tbody></table></div></div>`;
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) => `<tr>
<td class="num" style="color:var(--muted)">#${p.id}</td>
<td><span class="av" style="background:${avColor(p.name)}">${sicher(initialen(p.name))}</span>${sicher(p.name)}</td>
<td>${sicher(p.job)}${p.fraktion ? ` <span class="pill b">${sicher(p.fraktion)}</span>` : ""}</td>
<td style="color:var(--muted)">${sicher(p.region)}</td><td>${pingPill(p.ping)}</td>
<td class="num">${euro(p.cash + p.bank)}</td><td><span class="flags">${flags(p)}</span></td>
<td class="num"><button class="btn primary sm" data-p="${p.id}">Verwalten</button></td></tr>`).join("")
|| '<tr><td colspan="8" class="empty">Keine Spieler.</td></tr>';
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)
? `<button data-a="${a}" class="${extra || ""}">${icon} ${label}<small>${sub}</small></button>` : "";
modal(`<h3><span class="av" style="background:${avColor(p.name)};width:30px;height:30px">${sicher(initialen(p.name))}</span> ${sicher(p.name)} <span style="color:var(--muted);font-size:13px">#${p.id}</span></h3>
<div class="msub">${sicher(p.job)}${p.fraktion ? " · " + sicher(p.fraktion) : ""} · ${sicher(p.region)} · ${d.spielzeit || 0} Min · Gruppe ${sicher(d.gruppe)}</div>
<div class="grid" style="grid-template-columns:1fr 1fr;margin-bottom:10px">
<div class="card" style="padding:11px"><div class="kv"><span>Bargeld</span><span>${euro(p.cash)}</span></div><div class="kv"><span>Bank</span><span>${euro(p.bank)}</span></div><div class="kv"><span>Ping</span><span>${p.ping} ms</span></div></div>
<div class="card" style="padding:11px"><div class="kv"><span>Leben</span><span>${p.leben}</span></div><div class="kv"><span>Verwarnungen</span><span>${p.warns}</span></div><div class="kv"><span>Godmode</span><span>${p.god ? "an" : "aus"}</span></div></div></div>
<div class="actiongrid">
<div class="ag-h">Teleport & Ansicht</div>
${knopf("goto", "🧭", "Zu Spieler", "Teleport hin")}${knopf("bring", "📥", "Herholen", "zu mir")}
${knopf("spectate", "👁️", "Beobachten", "native")}${darf("kamera.oeffnen") ? '<button data-a="kamera">📹 Live-Kamera<small>WebRTC</small></button>' : ""}
<div class="ag-h">Zustand</div>
${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" : "")}
<div class="ag-h">Geben & Setzen</div>
${knopf("geld", "💵", "Geld", "geben/nehmen")}${knopf("item", "🎁", "Item", "geben")}${knopf("fahrzeug", "🚗", "Fahrzeug", "spawnen")}${knopf("job", "💼", "Job/Fraktion", "setzen")}
<div class="ag-h">Moderation</div>
${knopf("warn", "⚠️", "Verwarnen", "Warn")}${knopf("notiz", "📝", "Notiz", "intern")}
${knopf("kick", "👢", "Kicken", "trennen", "danger")}${knopf("ban", "🔨", "Bannen", "sperren", "danger")}
</div>
${d.warns.length ? `<div class="list-h" style="margin-top:12px">Verwarnungen</div>${d.warns.map((w) => `<div class="kv"><span>${sicher(w.grund)}</span><span style="color:var(--muted)">${sicher(w.admin_name)} · ${zeit(w.erstellt_am)}</span></div>`).join("")}` : ""}
<div class="modal-actions" style="grid-template-columns:1fr"><button class="btn ghost" id="pm-close">Schließen</button></div>`);
$("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,
`<label class="flabel">Konto</label><select class="input" id="x-konto"><option value="bank" selected>Bank</option><option value="cash">Bargeld</option></select>
<label class="flabel">Betrag (negativ = entfernen)</label><input class="input" id="x-betrag" type="number">`,
() => A("spieler", "geld", { id: p.id, konto: $("x-konto").value, betrag: +$("x-betrag").value }), p);
if (a === "item") return eingabeModal("Item geben · " + p.name,
`<label class="flabel">Item</label><input class="input" id="x-item" placeholder="z. B. water">
<label class="flabel">Menge</label><input class="input" id="x-menge" type="number" value="1">`,
() => 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,
`<label class="flabel">Modell (Spawnname)</label><input class="input" id="x-modell" placeholder="z. B. adder">`,
() => A("spieler", "fahrzeug", { id: p.id, modell: $("x-modell").value.trim() }), p);
if (a === "job") return eingabeModal("Job / Fraktion · " + p.name,
`<label class="flabel">Job</label><select class="input" id="x-job">${(d.jobs || []).map((j) => `<option ${p.job === j ? "selected" : ""}>${sicher(j)}</option>`).join("")}</select>
<label class="flabel">Fraktion (leer = keine)</label><input class="input" id="x-frak" value="${sicher(p.fraktion || "")}">`,
() => 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,
`<label class="flabel">Grund</label><input class="input" id="x-grund" placeholder="z. B. Fail RP">`,
() => A("spieler", "warn", { id: p.id, grund: $("x-grund").value.trim() }), p);
if (a === "notiz") return eingabeModal("Interne Notiz · " + p.name,
`<textarea class="input" id="x-text" placeholder="Notiz…"></textarea>`,
() => A("spieler", "notiz", { id: p.id, text: $("x-text").value.trim() }), p);
if (a === "kick") return eingabeModal("Kicken · " + p.name,
`<label class="flabel">Grund</label><input class="input" id="x-grund" placeholder="Grund">`,
() => A("spieler", "kick", { id: p.id, grund: $("x-grund").value.trim() }), null, "red");
if (a === "ban") return eingabeModal("Bannen · " + p.name,
`<label class="flabel">Grund</label><input class="input" id="x-grund" placeholder="z. B. Cheating">
<label class="flabel">Dauer</label><select class="input" id="x-dauer"><option>1 Tag</option><option>3 Tage</option><option selected>7 Tage</option><option>30 Tage</option><option>permanent</option></select>`,
() => 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(`<h3>${sicher(titel)}</h3>${felder}
<div class="modal-actions"><button class="btn ghost" id="e-c">Abbrechen</button><button class="btn ${farbe || "primary"}" id="e-ok">Ausführen</button></div>`, 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`) +
`<div style="display:grid;grid-template-columns:1fr 252px;gap:12px;height:calc(100% - 58px);min-height:340px">
<div class="map" id="map"><div class="water"></div>
<div class="distr" style="left:12%;top:14%">Vinewood</div><div class="distr" style="left:58%;top:20%">Downtown</div><div class="distr" style="left:20%;top:74%">Vespucci</div>
<div id="marker"></div></div>
<div style="overflow-y:auto"><div class="list-h">Spieler <span class="r">${d.spieler.length}</span></div><div id="mapliste"></div></div></div>`;
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) => `<div class="mk${p.tot ? " dead" : ""}" data-mk="${p.id}" style="left:${zx(p.x)}%;top:${zy(p.y)}%"><div class="dot" style="background:${frakFarbe(p.fraktion)}">${sicher(initialen(p.name))}</div><div class="lbl">${sicher(p.name.split(" ")[0])}</div></div>`).join("");
const ml = $("mapliste");
if (ml) ml.innerHTML = sp.map((p) => `<div class="row" data-pq="${p.id}" style="margin-bottom:6px;cursor:pointer"><span class="av" style="width:28px;height:28px;font-size:10px;background:${frakFarbe(p.fraktion)}">${sicher(initialen(p.name))}</span><div class="grow"><div class="t">${sicher(p.name)}</div><div class="m">${p.ping} ms</div></div></div>`).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`,
`<div style="display:flex;gap:8px">${darf("fahrzeuge.aufraeumen") ? '<button class="btn ghost" id="fz-clean">🧹 Leere aufräumen</button>' : ""}${darf("fahrzeuge.spawn") ? '<button class="btn primary" id="fz-spawn">+ Spawnen</button>' : ""}</div>`) +
`<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Kennzeichen</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>
${d.fahrzeuge.map((v) => `<tr><td class="mono">${sicher(v.plate || "—")}</td>
<td style="color:var(--muted)">${v.x.toFixed(0)}, ${v.y.toFixed(0)}</td>
<td>${v.leer ? '<span class="pill m">leer</span>' : '<span class="pill g">besetzt</span>'}</td>
<td class="num">${darf("fahrzeuge.reparieren") ? `<button class="btn ghost sm" data-fix="${v.netId}">Reparieren</button>` : ""}${darf("fahrzeuge.herholen") ? ` <button class="btn ghost sm" data-her="${v.netId}">Herholen</button>` : ""}${darf("fahrzeuge.loeschen") ? ` <button class="btn red sm" data-del="${v.netId}">Löschen</button>` : ""}</td></tr>`).join("") || '<tr><td colspan="4" class="empty">Keine Fahrzeuge.</td></tr>'}
</tbody></table></div></div>`;
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(`<h3>Fahrzeug spawnen</h3><label class="flabel">Für Spieler</label><select class="input" id="fs-ziel"><option value="">— du selbst —</option>${(main._spieler || []).map((s) => `<option value="${s.id}">${sicher(s.name)} (#${s.id})</option>`).join("")}</select>
<label class="flabel">Modell</label><input class="input" id="fs-modell" placeholder="z. B. adder">
<div class="modal-actions"><button class="btn ghost" id="fs-c">Abbrechen</button><button class="btn primary" id="fs-ok">Spawnen</button></div>`, 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") +
`<div class="grid" style="grid-template-columns:1fr 1fr">
<div class="card"><div class="list-h">🕒 Zeit</div>
<input type="range" min="0" max="23" value="12" id="w-stunde" style="width:100%;accent-color:var(--accent)">
<div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap"><button class="btn ghost sm" data-h="6">Morgen</button><button class="btn ghost sm" data-h="12">Mittag</button><button class="btn ghost sm" data-h="18">Abend</button><button class="btn ghost sm" data-h="0">Nacht</button></div>
<button class="btn ghost" id="w-zeitfix" style="width:100%;margin-top:10px">Zeit einfrieren (auf Reglerwert)</button></div>
<div class="card"><div class="list-h">🌦️ Wetter</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">${wetter.map((w) => `<button class="btn ghost sm" data-w="${w}">${w}</button>`).join("")}</div>
<button class="btn amber" id="w-blackout" style="width:100%;margin-top:10px">🌑 Blackout umschalten</button></div></div>
<div class="card" style="margin-top:12px"><div class="list-h">🧹 Bereinigung</div><div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn ghost" data-clean="veh">Leere Fahrzeuge</button><button class="btn ghost" data-clean="ped">NPCs</button><button class="btn ghost" data-clean="obj">Objekte</button><button class="btn ghost" data-clean="ground">Leichen</button></div></div>`;
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") +
`<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start"><div class="card">
<label class="flabel">Typ</label><select class="input" id="bc-typ"><option>Info</option><option>Warnung</option><option>Event</option></select>
<label class="flabel">Nachricht</label><textarea class="input" id="bc-msg" placeholder="An den ganzen Server…"></textarea>
<button class="btn primary" id="bc-send" style="width:100%;margin-top:12px">📢 Senden</button></div>
<div class="card"><div class="list-h">Verlauf</div>${(d.verlauf || []).map((b) => `<div class="kv"><span><span class="pill ${b.typ === "Warnung" ? "r" : b.typ === "Event" ? "v" : "b"}">${sicher(b.typ)}</span> ${sicher(b.nachricht)}</span><span style="color:var(--muted)">${zeit(b.erstellt_am)}</span></div>`).join("") || '<div class="empty">Noch keine.</div>'}</div></div>`;
$("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") ? '<button class="btn red" id="mod-ban">+ Manueller Ban</button>' : "") +
`<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start">
<div class="card"><div class="list-h">🔨 Aktive Bans</div>${d.bans.map((b) => `<div class="row" style="margin-bottom:6px"><div class="grow"><div class="t">${sicher(b.name)}</div><div class="m">${sicher(b.grund)} · ${b.bis ? "bis " + zeit(b.bis) : "permanent"} · ${sicher(b.admin_name)}</div></div>${darf("moderation.unban") ? `<button class="btn ghost sm" data-unban="${b.id}">Entbannen</button>` : ""}</div>`).join("") || '<div class="empty">Keine Bans.</div>'}</div>
<div class="card"><div class="list-h">⚠️ Verwarnungen</div>${d.warns.map((w) => `<div class="row" style="margin-bottom:6px"><div class="grow"><div class="t">${sicher(w.name)}</div><div class="m">${sicher(w.grund)} · ${sicher(w.admin_name)} · ${zeit(w.erstellt_am)}</div></div>${darf("moderation.warnWeg") ? `<button class="btn ghost sm" data-warn="${w.id}">Löschen</button>` : ""}</div>`).join("") || '<div class="empty">Keine Verwarnungen.</div>'}</div></div>`;
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(`<h3>Manueller Ban</h3><label class="flabel">Name oder Lizenz</label><input class="input" id="mb-name"><label class="flabel">Grund</label><input class="input" id="mb-grund"><label class="flabel">Dauer</label><select class="input" id="mb-dauer"><option>1 Tag</option><option>7 Tage</option><option>30 Tage</option><option selected>permanent</option></select><div class="modal-actions"><button class="btn ghost" id="mb-c">Abbrechen</button><button class="btn red" id="mb-ok">Bannen</button></div>`, 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`) +
`<div style="display:flex;flex-direction:column;gap:10px">${d.reports.map((r) => {
const pp = prioP[r.prioritaet] || ["?", "m"], sp = stP[r.status] || ["?", "m"];
return `<div class="card" style="display:flex;align-items:center;gap:16px;padding:14px 18px;border-left:3px solid var(--${pp[1] === "r" ? "red" : pp[1] === "a" ? "amber" : "accent"})">
<div class="grow"><div style="font-weight:800;font-size:14.5px">${sicher(r.betreff)}</div>
<div style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap"><span class="pill ${pp[1]}">${pp[0]}</span><span class="pill ${sp[1]}">${sp[0]}</span><span style="font-size:12px;color:var(--muted)">von <b style="color:var(--fg)">${sicher(r.melder_name)}</b>${r.bearbeiter ? " · " + sicher(r.bearbeiter) : ""}</span></div></div>
<div style="display:flex;gap:7px">${r.status === "geschlossen" ? `<button class="btn ghost sm" data-st="${r.id}:offen">Öffnen</button>` : `${darf("reports.claim") ? `<button class="btn ghost sm" data-claim="${r.id}">Übernehmen</button>` : ""}${darf("reports.antwort") ? `<button class="btn primary sm" data-ans="${r.id}:${sicher(r.melder_name)}">Antworten</button>` : ""}<button class="btn green sm" data-st="${r.id}:geschlossen">Schließen</button>`}</div></div>`;
}).join("") || '<div class="empty"><div class="big">🎫</div>Keine Reports.</div>'}</div>`;
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(`<h3>Antwort · ${sicher(name)}</h3><textarea class="input" id="ra" placeholder="Antwort an den Spieler…"></textarea><div class="modal-actions"><button class="btn ghost" id="ra-c">Abbrechen</button><button class="btn primary" id="ra-ok">Senden</button></div>`, 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") +
`<div class="grid" style="grid-template-columns:repeat(4,1fr)">
<div class="card kpi"><div class="l">💶 Geld im Umlauf</div><div class="v">${kompakt(d.umlauf)}</div><div class="d" style="color:var(--muted)">${d.charaktere} Charaktere</div></div>
<div class="card kpi"><div class="l">🏛️ Staatskasse</div><div class="v">${kompakt(d.staatskasse)}</div></div>
<div class="card kpi"><div class="l">👤 Ø Vermögen</div><div class="v">${kompakt(d.charaktere ? d.umlauf / d.charaktere : 0)}</div></div>
<div class="card kpi"><div class="l">📊 Steuersatz</div><div class="v">${d.steuer}%</div></div></div>
${darf("wirtschaft.geldAlle") ? `<div class="card" style="margin-top:12px"><div class="list-h">Aktionen</div><div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn ghost" id="e-all">💸 Geld an alle</button><button class="btn ghost" id="e-kasse">🏛️ Staatskasse</button><button class="btn ghost" id="e-steuer">📊 Steuersatz</button></div></div>` : ""}`;
const dialog = (titel, feld, aktion) => { modal(`<h3>${titel}</h3>${feld}<div class="modal-actions"><button class="btn ghost" id="w-c">Abbrechen</button><button class="btn primary" id="w-ok">Speichern</button></div>`, 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", `<label class="flabel">Betrag pro Spieler (Bank)</label><input class="input" id="wx" type="number">`, () => A("wirtschaft", "geldAlle", { betrag: +$("wx").value })));
const eK = $("e-kasse"); if (eK) eK.addEventListener("click", () => dialog("Staatskasse", `<label class="flabel">Neuer Stand</label><input class="input" id="wx" type="number" value="${d.staatskasse}">`, () => A("wirtschaft", "kasse", { wert: +$("wx").value })));
const eS = $("e-steuer"); if (eS) eS.addEventListener("click", () => dialog("Steuersatz", `<label class="flabel">Prozent</label><input class="input" id="wx" type="number" value="${d.steuer}">`, () => 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") ? '<button class="btn primary" id="fr-neu">+ Neue</button>' : "") +
`<div class="grid" style="grid-template-columns:repeat(2,1fr)">${d.fraktionen.map((f) => `<div class="card"><div style="display:flex;align-items:center;gap:10px"><span class="av" style="background:${sicher(f.farbe)}">${sicher(initialen(f.name))}</span><div class="grow"><div class="t" style="font-weight:800">${sicher(f.name)}</div><div class="m">${f.mitglieder} Mitglieder · ${f.online} online</div></div><span class="pill ${f.gehalt ? "g" : "m"}">${f.gehalt ? euro(f.gehalt) + " /h" : "kein Payday"}</span></div>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">${darf("fraktionen.speichern") ? `<button class="btn ghost sm" data-edit="${sicher(f.name)}">Bearbeiten</button>` : ""}${darf("fraktionen.payday") && f.gehalt ? `<button class="btn green sm" data-pay="${sicher(f.name)}">Payday</button>` : ""}${darf("fraktionen.loeschen") ? `<button class="btn red sm" data-del="${sicher(f.name)}">Löschen</button>` : ""}</div></div>`).join("") || '<div class="empty">Keine Fraktionen.</div>'}</div>`;
main._fr = d.fraktionen;
const frModal = (f) => { modal(`<h3>${f ? "Fraktion bearbeiten" : "Neue Fraktion"}</h3><label class="flabel">Name</label><input class="input" id="fn" value="${f ? sicher(f.name) : ""}" ${f ? "readonly" : ""}><label class="flabel">Gehalt (€/h)</label><input class="input" id="fg" type="number" value="${f ? f.gehalt : 0}"><label class="flabel">Farbe</label><input class="input" id="ff" value="${f ? sicher(f.farbe) : "#34d39a"}"><div class="modal-actions"><button class="btn ghost" id="f-c">Abbrechen</button><button class="btn primary" id="f-ok">Speichern</button></div>`, 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`) +
`<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Objekt</th><th>Lage</th><th>Besitzer</th><th class="num">Wert</th><th></th></tr></thead><tbody>
${d.immobilien.map((im) => `<tr><td>🏠 ${sicher(im.name)}</td><td style="color:var(--muted)">${sicher(im.ort || "—")}</td><td>${im.besitzerName ? sicher(im.besitzerName) : '<span class="pill m">frei</span>'}</td><td class="num">${euro(im.preis)}</td>
<td class="num">${darf("immobilien.zuweisen") ? `<button class="btn ghost sm" data-assign="${im.id}">Besitzer</button>` : ""}${im.besitzer && darf("immobilien.raeumen") ? ` <button class="btn red sm" data-evict="${im.id}">Räumen</button>` : ""}</td></tr>`).join("") || '<tr><td colspan="5" class="empty">Keine Objekte.</td></tr>'}
</tbody></table></div></div>`;
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(`<h3>Besitzer setzen</h3><label class="flabel">Online-Charakter</label><select class="input" id="im-o"><option value="">— frei —</option>${(d.spieler || []).map((s) => `<option value="${s.charakterId}">${sicher(s.name)}</option>`).join("")}</select><div class="modal-actions"><button class="btn ghost" id="im-c">Abbrechen</button><button class="btn primary" id="im-ok">Setzen</button></div>`, 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 ? '<span class="pill g">✔ Umgesetzt</span>' : p.offen ? '<span class="pill b">Abstimmung läuft</span>' : positiv ? '<span class="pill a">bereit</span>' : '<span class="pill r">abgelehnt</span>';
let aktion = "";
if (!p.aktiviert && p.offen && darf("staat.beenden")) aktion = `<button class="btn ghost sm" data-end="${p.id}">Abstimmung beenden</button>`;
else if (!p.aktiviert && !p.offen && positiv && darf("staat.aktivieren")) aktion = `<button class="btn green sm" data-act="${p.id}">Aktivieren</button>`;
else if (!p.aktiviert && !p.offen && !positiv && darf("staat.archivieren")) aktion = `<button class="btn ghost sm" data-arch="${p.id}">Archivieren</button>`;
return `<div class="card" style="margin-bottom:10px"><div style="display:flex;gap:12px"><div class="grow"><div style="font-weight:800;font-size:15px">${sicher(p.titel)} ${status}</div>
<div style="font-size:12.5px;color:var(--muted);margin:3px 0 8px">${sicher(p.beschreibung || "")} · Kosten: ${euro(p.kosten)}</div>
<div style="height:9px;border-radius:5px;background:var(--slot);overflow:hidden;display:flex"><div style="width:${p.zustimmung}%;background:var(--green)"></div><div style="width:${100 - p.zustimmung}%;background:var(--red)"></div></div>
<div style="font-size:11.5px;color:var(--muted);margin-top:5px">${p.ja} Dafür · ${p.nein} Dagegen · <b style="color:${positiv ? "var(--green)" : "var(--red)"}">${p.zustimmung}%</b> (nötig: ${d.zustimmungMin}%)</div></div>
<div style="display:flex;flex-direction:column;gap:6px">${aktion}</div></div></div>`;
}).join("") || '<div class="empty">Keine Projekte.</div>';
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") ? '<button class="btn red" id="rz-restart">⟳ Neustart</button>' : "") +
`<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Ressource</th><th>Status</th><th>Version</th><th></th></tr></thead><tbody>
${d.ressourcen.map((r, i) => `<tr><td>🧩 ${sicher(r.name)}</td><td>${r.status === "started" ? '<span class="pill g">läuft</span>' : '<span class="pill r">' + sicher(r.status) + "</span>"}</td><td style="color:var(--muted)">${sicher(r.version)}</td>
<td class="num">${darf("ressourcen.steuern") ? (r.status === "started" ? `<button class="btn ghost sm" data-r="${sicher(r.name)}:restart">Restart</button> <button class="btn red sm" data-r="${sicher(r.name)}:stop">Stop</button>` : `<button class="btn green sm" data-r="${sicher(r.name)}:start">Start</button>`) : ""}</td></tr>`).join("")}
</tbody></table></div></div>`;
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`) +
`<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
<div class="searchbar" style="flex:1;min-width:200px"><span>🔎</span><input id="lg-such" placeholder="Aktion, Ziel oder Detail…" value="${sicher(logSuche)}"></div>
<select class="input" id="lg-admin" style="width:auto"><option value="alle">Alle Admins</option>${(d.admins || []).map((a) => `<option ${a === logAdmin ? "selected" : ""}>${sicher(a)}</option>`).join("")}</select></div>
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Zeit</th><th>Admin</th><th>Aktion</th><th>Ziel</th><th>Detail</th></tr></thead><tbody>
${d.eintraege.map((l) => `<tr><td class="mono" style="color:var(--muted)">${zeit(l.erstellt_am)}</td><td>${sicher(l.admin_name)}</td><td>${sicher(l.aktion)}</td><td style="color:var(--muted)">${sicher(l.ziel || "")}</td><td style="color:var(--muted)">${sicher(l.detail || "")}</td></tr>`).join("") || '<tr><td colspan="5" class="empty">Keine Einträge.</td></tr>'}
</tbody></table></div></div>`;
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") ? '<button class="btn primary" id="tm-neu">+ Rolle vergeben</button>' : "") +
`<div class="grid" style="grid-template-columns:repeat(2,1fr)">${d.team.map((a) => `<div class="card"><div style="display:flex;align-items:center;gap:11px"><span class="av" style="width:40px;height:40px;background:${avColor(a.name)}">${sicher(initialen(a.name))}</span>
<div class="grow"><div style="font-weight:800">${sicher(a.name)}</div><div style="font-size:12px"><span class="pill ${rolleP[a.admin_group] || "m"}">${sicher(a.admin_group)}</span></div></div><span class="pill ${a.online ? "g" : "m"}">${a.online ? "online" : "offline"}</span></div>
${darf("team.entfernen") ? `<div style="display:flex;gap:8px;margin-top:12px"><button class="btn ghost sm" data-role="${sicher(a.license)}:${sicher(a.name)}">Rolle ändern</button><button class="btn red sm" data-del="${sicher(a.license)}:${sicher(a.name)}">Entfernen</button></div>` : ""}</div>`).join("") || '<div class="empty">Kein Team.</div>'}</div>
<div class="card" style="margin-top:12px;font-size:12.5px;color:var(--muted)">Die Rolle steht in <b style="color:var(--fg)">aether_users.admin_group</b> — der Core respektiert sie überall. <b style="color:var(--red)">owner</b> › <b style="color:var(--amber)">admin</b> › <b style="color:var(--accent)">support</b>.</div>`;
main._rollen = d.rollen;
const rollenModal = (lizenz, name) => { modal(`<h3>Rolle · ${sicher(name || "neuer Benutzer")}</h3>${lizenz ? "" : '<label class="flabel">Name oder Lizenz</label><input class="input" id="tr-name">'}<label class="flabel">Rolle</label><select class="input" id="tr-rolle">${main._rollen.map((r) => `<option>${r}</option>`).join("")}</select><div class="modal-actions"><button class="btn ghost" id="tr-c">Abbrechen</button><button class="btn primary" id="tr-ok">Speichern</button></div>`, 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) => `<div class="row" style="margin:0 0 8px"><div class="grow"><div class="t">${label}</div></div><button class="btn ${d.schalter[k] ? "green" : "ghost"} sm" data-set="${k}">${d.schalter[k] ? "AN" : "AUS"}</button></div>`;
main.innerHTML = kopf("Einstellungen", "Server-Konfiguration") +
`<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start"><div class="card"><div class="list-h">Allgemein</div>
<label class="flabel">Servername</label><input class="input" id="st-name" value="${sicher(d.servername)}">
<label class="flabel">Maximale Slots (aus Serverkonfig)</label><input class="input" value="${d.slots}" disabled>
<label class="flabel">Neustart-Intervall (Stunden)</label><input class="input" id="st-rs" type="number" value="${d.neustartStunden}">
<button class="btn primary sm" id="st-save" style="margin-top:12px">Speichern</button></div>
<div class="card"><div class="list-h">Regeln & Systeme</div>${sw("pvp", "PvP")}${sw("whitelist", "Whitelist")}${sw("voice", "Voice-Chat")}${sw("economy", "Wirtschaft")}</div></div>`;
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"); } });
});
})();