- 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>
489 lines
50 KiB
JavaScript
489 lines
50 KiB
JavaScript
/* =====================================================================
|
||
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"); } });
|
||
});
|
||
})();
|