- 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>
978 lines
111 KiB
HTML
978 lines
111 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark">
|
||
</head>
|
||
<body>
|
||
<title>Aether — Admin Dashboard</title>
|
||
<style>
|
||
:root{
|
||
--bg:#070a10;--fg:#e8ecf4;--muted:#7e879b;--surface:rgba(22,26,36,0.72);--surface2:rgba(34,39,52,0.85);
|
||
--slot:#111520;--border:rgba(255,255,255,0.08);--accent:#5b8cff;--ink:#04102b;--accent-soft:rgba(91,140,255,0.16);
|
||
--green:#34d39a;--amber:#f5b23d;--red:#f0506e;--violet:#a56bff;--cyan:#22c1d4;
|
||
}
|
||
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
|
||
body{min-height:100%;background:radial-gradient(1200px 700px at 15% 0%,rgba(91,140,255,0.08),transparent 55%),radial-gradient(1000px 800px at 90% 100%,rgba(52,211,153,0.05),transparent 55%),var(--bg);
|
||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);
|
||
display:flex;align-items:center;justify-content:center;padding:20px 14px;overflow-x:hidden;}
|
||
.admin{position:relative;width:min(1360px,98vw);aspect-ratio:16/10;border-radius:20px;overflow:hidden;
|
||
background:linear-gradient(160deg,rgba(18,22,30,0.92),rgba(9,11,16,0.96));backdrop-filter:blur(20px);border:1px solid var(--border);
|
||
box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);display:grid;grid-template-columns:224px 1fr;grid-template-rows:auto 1fr;grid-template-areas:"brand top" "nav main";}
|
||
.brand{grid-area:brand;padding:14px 18px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
|
||
.brand .bl{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;font-size:18px;background:linear-gradient(150deg,var(--accent),#2a52c9);}
|
||
.brand .bn{font-size:15px;font-weight:800;letter-spacing:-0.02em;line-height:1.1;} .brand .bn small{display:block;font-size:10px;color:var(--muted);font-weight:500;}
|
||
.top{grid-area:top;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;padding:0 18px;}
|
||
.srv-chip{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:700;} .srv-dot{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);}
|
||
.top .live{display:flex;gap:16px;margin-left:8px;font-size:12px;color:var(--muted);} .top .live b{color:var(--fg);font-variant-numeric:tabular-nums;}
|
||
.top .restart{margin-left:auto;font-size:12px;color:var(--amber);font-weight:700;}
|
||
.top .me{display:flex;align-items:center;gap:9px;padding-left:14px;border-left:1px solid var(--border);} .top .me .av{width:32px;height:32px;border-radius:50%;background:linear-gradient(150deg,var(--violet),#6b3fb0);display:grid;place-items:center;font-weight:800;font-size:12px;} .top .me .mn{font-size:13px;font-weight:700;} .top .me .mr{font-size:10.5px;color:var(--amber);font-weight:700;}
|
||
.nav{grid-area:nav;border-right:1px solid var(--border);padding:10px 9px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;}
|
||
.nav .grp{font-size:9px;text-transform:uppercase;letter-spacing:0.09em;color:#535c70;font-weight:800;margin:10px 12px 4px;}
|
||
.nav button{display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:9px;border:none;background:none;color:var(--muted);font-family:inherit;font-size:13px;font-weight:700;cursor:pointer;text-align:left;transition:.12s;}
|
||
.nav button .ni{font-size:15px;width:18px;text-align:center;} .nav button:hover{background:var(--surface);color:var(--fg);} .nav button.active{background:var(--accent-soft);color:var(--accent);}
|
||
.nav button .badge{margin-left:auto;background:var(--red);color:#fff;font-size:9.5px;font-weight:800;border-radius:999px;padding:1px 6px;}
|
||
.main{grid-area:main;overflow-y:auto;padding:18px 22px;} .main::-webkit-scrollbar,.nav::-webkit-scrollbar,.tblwrap::-webkit-scrollbar{width:7px;} .main::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.09);border-radius:4px;}
|
||
.phead{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:14px;} .phead h2{font-size:20px;margin:0;font-weight:800;letter-spacing:-0.02em;} .phead p{margin:2px 0 0;color:var(--muted);font-size:12.5px;}
|
||
|
||
.grid{display:grid;gap:12px;} .card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:15px;}
|
||
.kpi .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;display:flex;align-items:center;gap:6px;} .kpi .v{font-size:26px;font-weight:800;font-variant-numeric:tabular-nums;margin-top:3px;} .kpi .d{font-size:11.5px;font-weight:600;margin-top:1px;}
|
||
.list-h{font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);font-weight:800;margin:0 0 10px;display:flex;align-items:center;gap:8px;} .list-h .r{margin-left:auto;font-weight:600;text-transform:none;letter-spacing:0;}
|
||
.btn{border:none;border-radius:10px;padding:9px 14px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;transition:.12s;} .btn.primary{background:var(--accent);color:#fff;} .btn.primary:hover{filter:brightness(1.08);} .btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);} .btn.red{background:var(--red);color:#2a0910;} .btn.green{background:var(--green);color:#06231a;} .btn.amber{background:var(--amber);color:#3a2600;} .btn.sm{padding:6px 10px;font-size:12px;} .btn:disabled{opacity:.4;cursor:not-allowed;} .btn:active{transform:scale(0.98);}
|
||
.input,select.input,textarea.input{width:100%;background:var(--slot);border:1px solid var(--border);border-radius:9px;padding:10px 11px;color:var(--fg);font-size:13.5px;font-family:inherit;outline:none;} .input:focus{border-color:var(--accent);} textarea.input{resize:vertical;min-height:64px;}
|
||
.flabel{font-size:10.5px;color:var(--muted);font-weight:800;text-transform:uppercase;letter-spacing:0.04em;margin:11px 2px 5px;display:block;}
|
||
.searchbar{display:flex;align-items:center;gap:9px;background:var(--slot);border:1px solid var(--border);border-radius:10px;padding:9px 12px;} .searchbar input{flex:1;background:none;border:none;color:var(--fg);font-family:inherit;font-size:13.5px;outline:none;}
|
||
.seg{display:inline-flex;background:var(--slot);border-radius:9px;padding:3px;} .seg button{border:none;background:none;color:var(--muted);font-weight:700;font-size:12px;padding:6px 12px;border-radius:7px;cursor:pointer;font-family:inherit;} .seg button.active{background:var(--accent);color:#fff;}
|
||
|
||
.pill{font-size:10px;font-weight:800;padding:3px 8px;border-radius:999px;display:inline-block;} .pill.g{background:rgba(52,211,153,0.16);color:var(--green);} .pill.a{background:rgba(245,178,61,0.16);color:var(--amber);} .pill.r{background:rgba(240,80,110,0.16);color:var(--red);} .pill.b{background:rgba(91,140,255,0.16);color:var(--accent);} .pill.v{background:rgba(165,107,255,0.16);color:var(--violet);} .pill.m{background:rgba(255,255,255,0.08);color:var(--muted);}
|
||
.tblwrap{overflow-x:auto;} table.tbl{width:100%;border-collapse:collapse;font-size:12.5px;} .tbl th{text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;padding:8px 9px;border-bottom:1px solid var(--border);white-space:nowrap;} .tbl td{padding:9px 9px;border-bottom:1px solid var(--border);white-space:nowrap;} .tbl tr:last-child td{border:none;} .tbl tr:hover td{background:rgba(255,255,255,0.02);} .tbl .num{text-align:right;font-variant-numeric:tabular-nums;} .tbl .av{width:26px;height:26px;border-radius:50%;display:inline-grid;place-items:center;font-weight:800;color:#fff;font-size:10px;vertical-align:middle;margin-right:7px;}
|
||
.barmini{height:7px;border-radius:4px;background:var(--slot);overflow:hidden;width:90px;display:inline-block;vertical-align:middle;} .barmini>i{display:block;height:100%;border-radius:4px;}
|
||
.flags{display:inline-flex;gap:4px;}
|
||
|
||
.quick{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;} .quick button{justify-content:flex-start;text-align:left;}
|
||
.actiongrid .ag-h{grid-column:1/-1;font-size:9.5px;text-transform:uppercase;letter-spacing:0.07em;color:var(--muted);font-weight:800;margin:10px 2px 0;} .actiongrid .ag-h:first-child{margin-top:0;}
|
||
.vehgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;max-height:320px;overflow-y:auto;padding-right:4px;}
|
||
.vehcard{background:var(--slot);border:1px solid var(--border);border-radius:11px;padding:8px;cursor:pointer;font-family:inherit;color:var(--fg);transition:.12s;text-align:center;} .vehcard:hover{border-color:var(--accent);transform:translateY(-2px);}
|
||
.vehimg{aspect-ratio:16/10;border-radius:8px;overflow:hidden;background:linear-gradient(180deg,#1b2432,#0d1119);display:grid;place-items:center;} .vehimg svg{width:92%;}
|
||
.vehname{font-size:11.5px;font-weight:700;margin-top:6px;}
|
||
.veh-thumb{display:inline-block;width:46px;height:26px;vertical-align:middle;margin-right:8px;} .veh-thumb svg{width:100%;height:100%;}
|
||
.sw2{width:30px;height:30px;border-radius:8px;cursor:pointer;}
|
||
.qmenu{position:fixed;z-index:200;min-width:180px;background:rgba(16,20,28,0.98);border:1px solid var(--border);border-radius:12px;padding:5px;display:none;box-shadow:0 20px 50px -12px rgba(0,0,0,0.8);} .qmenu.show{display:block;animation:qm .12s ease;} @keyframes qm{from{opacity:0;transform:scale(.96);}to{opacity:1;}}
|
||
.qmenu .qm-h{padding:7px 10px 6px;font-size:12px;font-weight:800;border-bottom:1px solid var(--border);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||
.qmenu button{display:flex;align-items:center;gap:9px;width:100%;text-align:left;border:none;background:none;color:var(--fg);font-family:inherit;font-size:13px;font-weight:600;padding:8px 10px;border-radius:8px;cursor:pointer;} .qmenu button:hover{background:var(--surface2);} .qmenu button.danger{color:var(--red);}
|
||
.actiongrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:6px;} .actiongrid button{display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding:10px;border-radius:10px;background:var(--slot);border:1px solid var(--border);color:var(--fg);font-family:inherit;cursor:pointer;font-weight:700;font-size:12px;transition:.12s;} .actiongrid button:hover{border-color:var(--accent);} .actiongrid button small{color:var(--muted);font-weight:500;font-size:10px;} .actiongrid button.on{border-color:var(--green);background:rgba(52,211,153,0.1);} .actiongrid button.danger:hover{border-color:var(--red);}
|
||
|
||
.chk{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:9px;background:var(--slot);border:1px solid var(--border);margin-bottom:5px;cursor:pointer;} .chk.sel{border-color:var(--accent);background:var(--accent-soft);} .chk .av{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;font-weight:800;color:#fff;font-size:10px;flex-shrink:0;}
|
||
/* Weltkarte */
|
||
.map{position:relative;border-radius:14px;overflow:hidden;border:1px solid var(--border);height:100%;
|
||
background:linear-gradient(90deg,transparent 49.6%,rgba(255,255,255,0.04) 49.6% 50.4%,transparent 50.4%),linear-gradient(0deg,transparent 49.6%,rgba(255,255,255,0.04) 49.6% 50.4%,transparent 50.4%),repeating-linear-gradient(90deg,rgba(255,255,255,0.02) 0 1px,transparent 1px 60px),repeating-linear-gradient(0deg,rgba(255,255,255,0.02) 0 1px,transparent 1px 60px),linear-gradient(135deg,#101823,#0a0f16);}
|
||
.map .water{position:absolute;right:-6%;bottom:-8%;width:44%;height:52%;background:radial-gradient(circle at 70% 70%,rgba(40,90,140,0.5),transparent 70%);border-radius:50% 0 0 0;}
|
||
.map .distr{position:absolute;font-size:10px;font-weight:800;letter-spacing:0.1em;color:rgba(255,255,255,0.14);text-transform:uppercase;}
|
||
.mk{position:absolute;transform:translate(-50%,-50%);z-index:2;cursor:pointer;transition:left 2.8s linear,top 2.8s linear;}
|
||
.mk .dot{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:9px;font-weight:800;color:#fff;border:2px solid rgba(255,255,255,0.85);box-shadow:0 2px 6px rgba(0,0,0,0.6);transition:transform .2s;}
|
||
.mk .lbl{position:absolute;top:24px;left:50%;transform:translateX(-50%);white-space:nowrap;font-size:9px;font-weight:700;background:rgba(0,0,0,0.6);padding:1px 5px;border-radius:5px;}
|
||
.mk.dead .dot{background:var(--red)!important;animation:mkp 1.2s infinite;} @keyframes mkp{0%,100%{box-shadow:0 0 0 0 rgba(240,80,110,0.5);}50%{box-shadow:0 0 0 9px rgba(240,80,110,0);}}
|
||
/* Live-Kameras */
|
||
.camgrid{display:grid;gap:10px;} .camtile{position:relative;aspect-ratio:16/9;border-radius:12px;overflow:hidden;border:1px solid var(--border);background:#05070b;}
|
||
.camfeed{position:absolute;inset:0;} .camfeed .scene{position:absolute;inset:0;} .camfeed .scan{position:absolute;inset:0;background:repeating-linear-gradient(0deg,rgba(0,0,0,0.14) 0 2px,transparent 2px 4px);pointer-events:none;}
|
||
.camfeed .noise{position:absolute;inset:0;opacity:0.05;background-image:radial-gradient(rgba(255,255,255,0.8) 1px,transparent 1px);background-size:3px 3px;animation:noise .3s steps(2) infinite;} @keyframes noise{to{transform:translate(1px,1px);}}
|
||
.camfeed .sweep{position:absolute;left:0;right:0;height:40%;background:linear-gradient(180deg,rgba(255,255,255,0.05),transparent);animation:sweep 4s linear infinite;} @keyframes sweep{0%{top:-40%;}100%{top:100%;}}
|
||
.cam-hud{position:absolute;inset:0;z-index:2;font-family:ui-monospace,monospace;pointer-events:none;}
|
||
.cam-hud .rec{position:absolute;top:8px;right:10px;display:flex;align-items:center;gap:6px;font-size:10px;font-weight:800;color:#ff5a6e;} .cam-hud .rec i{width:8px;height:8px;border-radius:50%;background:#ff5a6e;animation:blink 1s infinite;} @keyframes blink{50%{opacity:0.2;}}
|
||
.cam-hud .who{position:absolute;top:8px;left:10px;font-size:11px;font-weight:800;color:#fff;text-shadow:0 1px 2px #000;}
|
||
.cam-hud .foot{position:absolute;bottom:8px;left:10px;right:10px;display:flex;justify-content:space-between;gap:8px;font-size:9.5px;color:rgba(255,255,255,0.85);text-shadow:0 1px 2px #000;}
|
||
.cam-hud .cross{position:absolute;top:50%;left:50%;width:26px;height:26px;transform:translate(-50%,-50%);opacity:0.55;} .cam-hud .cross::before,.cam-hud .cross::after{content:"";position:absolute;background:rgba(255,255,255,0.7);} .cam-hud .cross::before{left:50%;top:0;bottom:0;width:1px;transform:translateX(-50%);} .cam-hud .cross::after{top:50%;left:0;right:0;height:1px;transform:translateY(-50%);}
|
||
.camtile .ctrls{position:absolute;bottom:8px;right:8px;z-index:3;display:flex;gap:5px;} .camtile .ctrls button{border:none;background:rgba(0,0,0,0.55);color:#fff;border-radius:7px;padding:4px 8px;font-family:inherit;font-size:11px;font-weight:700;cursor:pointer;} .camtile .ctrls button:hover{background:rgba(0,0,0,0.85);}
|
||
.viewlist>button{width:100%;text-align:left;margin-bottom:6px;}
|
||
/* Killcams */
|
||
.kc{display:flex;gap:12px;align-items:center;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:10px;margin-bottom:8px;}
|
||
.kc .thumb{width:120px;aspect-ratio:16/9;border-radius:8px;overflow:hidden;flex-shrink:0;position:relative;border:1px solid var(--border);} .kc .thumb .scan{position:absolute;inset:0;background:repeating-linear-gradient(0deg,rgba(0,0,0,0.18) 0 2px,transparent 2px 4px);} .kc .thumb .pl{position:absolute;inset:0;display:grid;place-items:center;color:rgba(255,255,255,0.85);font-size:20px;} .kc .grow{flex:1;min-width:0;}
|
||
|
||
.overlay{position:absolute;inset:0;z-index:40;background:rgba(4,6,10,0.72);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;padding:22px;} .overlay.show{display:flex;}
|
||
.modal{width:560px;max-width:97%;max-height:90%;overflow-y:auto;background:rgba(18,22,30,0.98);border:1px solid var(--border);border-radius:16px;padding:20px;box-shadow:0 30px 70px -18px rgba(0,0,0,0.8);} .modal.sm{width:400px;}
|
||
.modal h3{margin:0 0 3px;font-size:18px;} .modal .msub{color:var(--muted);font-size:12.5px;margin-bottom:12px;} .modal-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:14px;}
|
||
.kv{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;border-bottom:1px solid var(--border);} .kv:last-child{border:none;} .kv span:first-child{color:var(--muted);}
|
||
.toast{position:absolute;bottom:16px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(12,15,22,0.97);border:1px solid var(--border);color:var(--fg);padding:10px 18px;border-radius:11px;font-size:13px;font-weight:600;z-index:50;opacity:0;pointer-events:none;transition:.3s;} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} .toast.good{border-color:var(--green);} .toast.warn{border-color:var(--red);}
|
||
.empty{text-align:center;color:var(--muted);padding:36px 10px;} .empty .big{font-size:36px;margin-bottom:8px;}
|
||
.chart svg{width:100%;display:block;}
|
||
/* Zeilen-Layout (Moderation, Weltkarte, Einstellungen) */
|
||
.row{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:9px;background:var(--slot);border:1px solid var(--border);}
|
||
.row .grow,.grow{flex:1;min-width:0;}
|
||
.row .t{font-weight:700;font-size:13px;} .row .m{font-size:11.5px;color:var(--muted);margin-top:1px;}
|
||
.mono{font-family:ui-monospace,"SF Mono",Menlo,monospace;font-variant-numeric:tabular-nums;}
|
||
.swatches{display:flex;flex-wrap:wrap;gap:8px;}
|
||
</style>
|
||
|
||
<div class="admin">
|
||
<div class="brand"><span class="bl">🛡️</span><div class="bn">Aether Admin<small>Server-Verwaltung</small></div></div>
|
||
<div class="top" id="top"></div>
|
||
<div class="nav" id="nav"></div>
|
||
<div class="main" id="main"></div>
|
||
<div class="overlay" id="overlay"></div>
|
||
<div class="toast" id="toast"></div>
|
||
</div>
|
||
<div class="qmenu" id="qmenu"></div>
|
||
|
||
<script>
|
||
"use strict";
|
||
const $=id=>document.getElementById(id);
|
||
function sicher(t){const d=document.createElement("div");d.textContent=t==null?"":t;return d.innerHTML;}
|
||
const euro=n=>Math.round(n).toLocaleString("de-DE")+" €";
|
||
const kompakt=n=>n>=1e6?(n/1e6).toFixed(1)+" Mio":n>=1e3?(n/1e3).toFixed(1)+"k":""+n;
|
||
let toastTimer=null; function toast(t,typ){const e=$("toast");e.textContent=t;e.className="toast show"+(typ?" "+typ:"");clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2400);}
|
||
function modal(html,sm){$("overlay").innerHTML=`<div class="modal${sm?' sm':''}">${html}</div>`;$("overlay").classList.add("show");}
|
||
function closeModal(){$("overlay").classList.remove("show");$("overlay").innerHTML="";}
|
||
const initialen=n=>n.replace(/\s*\(.*\)/,"").split(" ").map(w=>w[0]).slice(0,2).join("");
|
||
const avColor=(n)=>{let h=0;for(const c of n)h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 45% 45%)`;};
|
||
|
||
/* =====================================================================
|
||
ZUSTAND
|
||
===================================================================== */
|
||
const admin={name:"Owner",rolle:"Owner"};
|
||
const server={name:"Aether RP",slots:64,uptimeMin:1187,tps:19.4,cpu:38,ram:58,neustart:214,version:"v2.4.1"};
|
||
const welt={stunde:14,minute:32,wetter:"Klar",blackout:false,zeitFix:false,wetterFix:false};
|
||
const economy={umlauf:184300000,staatskasse:12480000,steuer:8};
|
||
let players=[
|
||
{id:1,name:"Max Weber",job:"Spediteur",fraktion:null,cash:1240,bank:48320,ping:38,region:"Legion Square",spielzeit:142,warns:0,god:false,frozen:false,muted:false},
|
||
{id:2,name:"Lena Berger",job:"Barkeeperin",fraktion:null,cash:640,bank:12400,ping:52,region:"Vespucci",spielzeit:88,warns:1,god:false,frozen:false,muted:false},
|
||
{id:3,name:"Dana Cole",job:"Polizist",fraktion:"LSPD",cash:2100,bank:167200,ping:29,region:"Mission Row",spielzeit:410,warns:0,god:false,frozen:false,muted:false},
|
||
{id:4,name:"Sofia Klein",job:"Sanitäterin",fraktion:"EMS",cash:900,bank:74000,ping:41,region:"Central Hospital",spielzeit:220,warns:0,god:false,frozen:false,muted:false},
|
||
{id:5,name:"Vincent Cross",job:"Arbeitslos",fraktion:"Ballas",cash:8400,bank:2100,ping:88,region:"Grove Street",spielzeit:64,warns:3,god:false,frozen:false,muted:true},
|
||
{id:6,name:"Karl Mendez",job:"Mechaniker",fraktion:null,cash:1500,bank:22140,ping:63,region:"Sandy Shores",spielzeit:156,warns:0,god:false,frozen:false,muted:false},
|
||
{id:7,name:"Mia Chen",job:"Polizist",fraktion:"LSPD",cash:800,bank:54300,ping:34,region:"Vinewood",spielzeit:98,warns:0,god:false,frozen:false,muted:false},
|
||
{id:8,name:"Ben Fischer",job:"Mechaniker",fraktion:null,cash:300,bank:9800,ping:120,region:"LS Customs",spielzeit:47,warns:0,god:false,frozen:false,muted:false},
|
||
{id:9,name:"Ava Stone",job:"Anwältin",fraktion:null,cash:5200,bank:210000,ping:45,region:"Del Perro",spielzeit:302,warns:0,god:true,frozen:false,muted:false},
|
||
{id:10,name:"Rico Vargas",job:"Arbeitslos",fraktion:"Ballas",cash:15600,bank:400,ping:150,region:"Grove Street",spielzeit:33,warns:2,god:false,frozen:true,muted:false},
|
||
{id:11,name:"Tom Barker",job:"Polizist",fraktion:"LSPD",cash:1900,bank:143900,ping:31,region:"Highway 68",spielzeit:388,warns:0,god:false,frozen:false,muted:false},
|
||
{id:12,name:"Nora Vale",job:"Journalistin",fraktion:null,cash:2700,bank:33000,ping:57,region:"Downtown",spielzeit:129,warns:0,god:false,frozen:false,muted:false}
|
||
];
|
||
let nextPid=13;
|
||
const jobs=["Arbeitslos","Spediteur","Mechaniker","Barkeeperin","Anwältin","Journalistin","Polizist","Sanitäterin","Taxifahrer"];
|
||
let fraktionen=[
|
||
{name:"LSPD",mitglieder:14,gehalt:2500,farbe:"#4aa8ff"},
|
||
{name:"EMS",mitglieder:9,gehalt:2200,farbe:"#f0506e"},
|
||
{name:"Ballas",mitglieder:11,gehalt:0,farbe:"#7c5cf6"},
|
||
{name:"Vagos",mitglieder:8,gehalt:0,farbe:"#f5b23d"},
|
||
{name:"Mechaniker-Gilde",mitglieder:6,gehalt:1800,farbe:"#34d39a"}
|
||
];
|
||
const fahrzeugModelle=["Sultan RS","Adder","Bati 801","Baller","Kuruma","Bison","Police Cruiser","Ambulance","Taxi","Sanchez"];
|
||
let weltFahrzeuge=[
|
||
{model:"Sultan RS",plate:"46EEK572",owner:"Max Weber",zustand:85},
|
||
{model:"Police Cruiser",plate:"LSPD-114",owner:"Dana Cole",zustand:97},
|
||
{model:"Ambulance",plate:"EMS-41",owner:"Sofia Klein",zustand:100},
|
||
{model:"Bati 801",plate:"88KLM214",owner:"Rico Vargas",zustand:62},
|
||
{model:"Adder",plate:"09HGT456",owner:"Ava Stone",zustand:78}
|
||
];
|
||
// deterministische Farbe je Modellname
|
||
function vehColor(name){let h=0;for(const c of name)h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 55% 48%)`;}
|
||
// Fahrzeug-Katalog mit Kategorien für den Spawner
|
||
const fahrzeugKatalog=[
|
||
{kat:"Sportwagen",typ:"sport",items:["Sultan RS","Adder","Kuruma","Elegy RH8","Comet"]},
|
||
{kat:"Limousinen",typ:"sedan",items:["Tailgater","Asterope","Primo","Fugitive"]},
|
||
{kat:"SUV & Pickup",typ:"suv",items:["Baller","Cavalcade","Granger","Bison","Sadler"]},
|
||
{kat:"Motorräder",typ:"bike",items:["Bati 801","Sanchez","Akuma","Hakuchou"]},
|
||
{kat:"Einsatz",typ:"sedan",items:["Police Cruiser","FIB","Ambulance","Feuerwehr"]},
|
||
{kat:"Helikopter",typ:"heli",items:["Maverick","Buzzard","Frogger","Polmav"]},
|
||
{kat:"Boote",typ:"boat",items:["Dinghy","Speeder","Marquis","Jetmax"]},
|
||
{kat:"Flugzeuge",typ:"plane",items:["Luxor","Duster","Titan","Mammatus"]}
|
||
];
|
||
// schematisches Fahrzeug-Icon je Typ (für Spawner-Bilder & Tabelle)
|
||
function fahrzeugIcon(typ,farbe){
|
||
const wh=(cx,cy,r)=>`<circle cx="${cx}" cy="${cy}" r="${r}" fill="#14161b"/><circle cx="${cx}" cy="${cy}" r="${(r*0.42).toFixed(1)}" fill="#4a505c"/>`;
|
||
const sh=`<ellipse cx="100" cy="98" rx="80" ry="6" fill="rgba(0,0,0,0.25)"/>`;
|
||
if(typ==="bike")return `<svg viewBox="0 0 200 110">${sh}<path d="M58 82 L96 54 L124 54 L148 82" stroke="#2a2e37" stroke-width="7" fill="none" stroke-linecap="round"/><path d="M80 58 C94 48 118 48 128 60 L130 68 L74 70 Z" fill="${farbe}"/><path d="M138 46 C158 50 164 70 152 86 L140 80 C150 68 146 56 132 54 Z" fill="${farbe}"/>${wh(60,82,20)}${wh(146,82,20)}</svg>`;
|
||
if(typ==="heli")return `<svg viewBox="0 0 200 110">${sh}<line x1="18" y1="38" x2="182" y2="38" stroke="#3a3f4a" stroke-width="4" stroke-linecap="round"/><rect x="96" y="38" width="6" height="10" fill="#2a2e37"/><path d="M42 62 Q60 46 98 48 L150 54 Q170 58 150 72 L72 76 Q46 76 42 62 Z" fill="${farbe}"/><path d="M150 54 L188 52 L188 60 L154 64 Z" fill="${farbe}"/><rect x="183" y="42" width="6" height="26" rx="2" fill="#2a2e37"/><path d="M72 76 L66 90 M118 74 L124 90" stroke="#2a2e37" stroke-width="4" stroke-linecap="round"/><path d="M58 92 h74" stroke="#3a3f4a" stroke-width="4" stroke-linecap="round"/></svg>`;
|
||
if(typ==="boat")return `<svg viewBox="0 0 200 110">${sh}<path d="M28 66 L172 66 L150 90 Q150 94 144 94 L52 94 Q46 94 44 88 Z" fill="${farbe}"/><path d="M72 48 L140 48 Q148 48 148 58 L148 64 L72 64 Z" fill="${farbe}"/><rect x="80" y="52" width="58" height="10" fill="rgba(8,14,22,0.85)"/><path d="M14 100 q10 -6 20 0 t20 0 t20 0 t20 0 t20 0 t20 0 t20 0 t20 0" stroke="#2a5a7a" stroke-width="3" fill="none"/></svg>`;
|
||
if(typ==="plane")return `<svg viewBox="0 0 200 110">${sh}<path d="M24 66 Q40 58 70 58 L156 60 Q178 62 178 68 Q178 74 156 74 L70 74 Q40 74 24 66 Z" fill="${farbe}"/><path d="M62 62 L112 30 L124 32 L94 66 Z" fill="${farbe}"/><path d="M62 70 L112 100 L124 98 L94 68 Z" fill="${farbe}"/><path d="M158 60 L178 44 L184 46 L172 64 Z" fill="${farbe}"/><circle cx="150" cy="66" r="4" fill="rgba(8,14,22,0.85)"/><circle cx="130" cy="66" r="4" fill="rgba(8,14,22,0.85)"/></svg>`;
|
||
const tall=typ==="suv"||typ==="pickup",rt=tall?30:40;
|
||
return `<svg viewBox="0 0 200 110">${sh}<path d="M20 80 C18 62 34 58 54 56 L74 ${rt+8} C88 ${rt} 120 ${rt} 134 ${rt+10} L166 58 C182 62 190 70 190 82 L190 86 L20 86 Z" fill="${farbe}"/><path d="M80 ${rt+10} C92 ${rt+2} 116 ${rt+2} 128 ${rt+12} L146 58 L76 58 Z" fill="rgba(8,14,22,0.85)"/>${wh(58,84,15)}${wh(150,84,15)}</svg>`;
|
||
}
|
||
// Standardwerte (Typ/Farbe/Tuning) für vorhandene Weltfahrzeuge
|
||
weltFahrzeuge.forEach(v=>{v.typ=v.typ||({"Sultan RS":"sport","Police Cruiser":"sedan","Ambulance":"suv","Bati 801":"bike","Adder":"sport"}[v.model]||"sedan");v.farbe=v.farbe||vehColor(v.model);v.tuning=v.tuning||{motor:2,bremsen:1,getriebe:1,turbo:false,panzerung:0};});
|
||
const immobilien=[
|
||
{name:"Apartment 4B",ort:"Vespucci Blvd",besitzer:"Max Weber",preis:95000},
|
||
{name:"Villa Vinewood Hills",ort:"Vinewood Hills",besitzer:"Ava Stone",preis:1200000},
|
||
{name:"Loft Downtown",ort:"Downtown",besitzer:null,preis:145000},
|
||
{name:"Bungalow Sandy Shores",ort:"Sandy Shores",besitzer:"Karl Mendez",preis:68000}
|
||
];
|
||
let bans=[
|
||
{name:"XxToxicxX",grund:"Cheating (Aimbot)",admin:"Owner",dauer:"permanent",datum:"19.07.2026"},
|
||
{name:"RDM_King",grund:"RDM / Regelbruch",admin:"Admin_Sara",dauer:"7 Tage",datum:"20.07.2026"}
|
||
];
|
||
let warns=[
|
||
{name:"Vincent Cross",grund:"Beleidigung im Voice",admin:"Mod_Ben",datum:"20.07.2026"},
|
||
{name:"Rico Vargas",grund:"Fail RP",admin:"Owner",datum:"21.07.2026"}
|
||
];
|
||
let reports=[
|
||
{id:"r1",spieler:"Lena Berger",betreff:"Spieler rammt absichtlich",prio:2,status:"offen"},
|
||
{id:"r2",spieler:"Ben Fischer",betreff:"Bug: Fahrzeug verschwunden",prio:3,status:"offen"},
|
||
{id:"r3",spieler:"Nora Vale",betreff:"Möglicher Cheater bei Grove St.",prio:1,status:"in Bearbeitung"}
|
||
];
|
||
let ressourcen=[
|
||
{name:"aether-core",status:"running",cpu:4.2,mem:82},
|
||
{name:"aether-phone",status:"running",cpu:2.1,mem:46},
|
||
{name:"aether-inventory",status:"running",cpu:3.4,mem:64},
|
||
{name:"aether-mdt",status:"running",cpu:1.2,mem:28},
|
||
{name:"aether-housing",status:"running",cpu:0.8,mem:22},
|
||
{name:"aether-anticheat",status:"running",cpu:5.6,mem:71},
|
||
{name:"legacy-fuel",status:"stopped",cpu:0,mem:0}
|
||
];
|
||
const adminTeam=[
|
||
{name:"Owner",rolle:"Owner",online:true},
|
||
{name:"Admin_Sara",rolle:"Admin",online:true},
|
||
{name:"Mod_Ben",rolle:"Moderator",online:true},
|
||
{name:"Supporter_Jo",rolle:"Supporter",online:false}
|
||
];
|
||
const spieler24h=[18,14,12,10,9,11,16,22,28,31,35,38,40,39,42,44,41,43,46,48,45,42,40,42];
|
||
const geldVerlauf=[171,173,176,178,180,181,183,184].map(v=>v*1e6);
|
||
// Live-Positionen für die Weltkarte
|
||
players.forEach((p,i)=>{p.posX=12+((i*29+13)%76);p.posY=16+((i*41+9)%68);p.tot=false;});
|
||
// Staatsprojekte (Abstimmungsergebnisse aus der Handy-Staat-App)
|
||
const ZUSTIMMUNG_MIN=0.6;
|
||
let staatsprojekte=[
|
||
{id:"sp1",titel:"Neue Feuerwache Sandy Shores",beschreibung:"Bau einer Rettungswache im Norden.",kosten:450000,ja:128,nein:34,abgelaufen:true,aktiviert:false},
|
||
{id:"sp2",titel:"Ausbau Autobahn N1",beschreibung:"Vierspuriger Ausbau der Nord-Autobahn.",kosten:1200000,ja:87,nein:96,abgelaufen:true,aktiviert:false},
|
||
{id:"sp3",titel:"Steuersenkung für Kleingewerbe",beschreibung:"Senkung der Gewerbesteuer um 3%.",kosten:300000,ja:203,nein:41,abgelaufen:false,aktiviert:false},
|
||
{id:"sp4",titel:"Öffentlicher Nahverkehr",beschreibung:"Neue Buslinien in Los Santos.",kosten:680000,ja:156,nein:52,abgelaufen:true,aktiviert:true}
|
||
];
|
||
// Kill-/Deathcams (nicht gespeicherte werden nach 24 h automatisch gelöscht)
|
||
const waffenPool=["Pistole P99","SMG","Schrotflinte","Sturmgewehr","Kampfmesser","Scharfschützengewehr"];
|
||
let killcams=[
|
||
{id:"kc1",killer:"Rico Vargas",opfer:"Ben Fischer",waffe:"SMG",distanz:14,ort:"Grove Street",zeit:Date.now()-1800000,gespeichert:false},
|
||
{id:"kc2",killer:"Vincent Cross",opfer:"Mia Chen",waffe:"Pistole P99",distanz:6,ort:"Vinewood Blvd",zeit:Date.now()-9000000,gespeichert:true},
|
||
{id:"kc3",killer:"Dana Cole",opfer:"Rico Vargas",waffe:"Sturmgewehr",distanz:31,ort:"Grove Street",zeit:Date.now()-3600000,gespeichert:false},
|
||
{id:"kc4",killer:"Unbekannt",opfer:"Karl Mendez",waffe:"Scharfschützengewehr",distanz:184,ort:"Sandy Shores",zeit:Date.now()-90000000,gespeichert:false},
|
||
{id:"kc5",killer:"Rico Vargas",opfer:"Tom Barker",waffe:"Schrotflinte",distanz:4,ort:"Grove Street",zeit:Date.now()-100000000,gespeichert:false}
|
||
];
|
||
// Live-Kamera-Views (Multi-Cam für z. B. Gang-Fights)
|
||
let camViews=[{id:"cv1",name:"Gang Fight Grove St.",spieler:[5,10]}];
|
||
let aktiveView="cv1", kcFilter="alle";
|
||
|
||
const logs=[];
|
||
function log(aktion,detail){logs.unshift({zeit:new Date(),wer:admin.name,aktion,detail:detail||""});}
|
||
log("Server gestartet","Aether RP "+server.version);
|
||
|
||
/* Helfer */
|
||
const player=id=>players.find(p=>p.id===id);
|
||
function pingPill(p){const c=p<50?'g':p<90?'a':'r';return `<span class="pill ${c}">${p} ms</span>`;}
|
||
function playerFlags(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>`;return f||'<span class="pill m">—</span>';}
|
||
|
||
/* =====================================================================
|
||
CHART (kleines Flächen-Diagramm)
|
||
===================================================================== */
|
||
function areaChart(werte,W,H,farbe,fmt){
|
||
const min=Math.min(...werte),max=Math.max(...werte),span=(max-min)||1,pL=6,pT=6,pB=6;
|
||
const x=i=>pL+(W-pL*2)*(i/(werte.length-1)), y=v=>pT+(H-pT-pB)*(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 area=d+` L${x(werte.length-1).toFixed(1)} ${H-pB} L${x(0).toFixed(1)} ${H-pB} Z`;
|
||
const uid="a"+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="${area}" fill="url(#${uid})"/><path d="${d}" fill="none" stroke="${farbe}" stroke-width="2" vector-effect="non-scaling-stroke"/>
|
||
<circle cx="${x(werte.length-1).toFixed(1)}" cy="${y(werte[werte.length-1]).toFixed(1)}" r="3.5" fill="${farbe}"/></svg>`;
|
||
}
|
||
|
||
/* =====================================================================
|
||
SHELL
|
||
===================================================================== */
|
||
let sektion="dashboard", mapTimer=null;
|
||
const nav=[
|
||
{grp:"Überblick"},{id:"dashboard",label:"Dashboard",icon:"📊"},{id:"weltkarte",label:"Weltkarte",icon:"🗺️"},
|
||
{grp:"Verwaltung"},{id:"spieler",label:"Spieler",icon:"👥"},{id:"fahrzeuge",label:"Fahrzeuge",icon:"🚗"},{id:"immobilien",label:"Immobilien",icon:"🏠"},{id:"fraktionen",label:"Fraktionen",icon:"🚔"},{id:"wirtschaft",label:"Wirtschaft",icon:"💰"},{id:"staat",label:"Staatsprojekte",icon:"🏛️"},
|
||
{grp:"Überwachung"},{id:"cams",label:"Live-Kameras",icon:"📹"},{id:"killcams",label:"Kill-/Deathcams",icon:"🎬"},
|
||
{grp:"Welt"},{id:"welt",label:"Weltsteuerung",icon:"🌍"},{id:"broadcast",label:"Ankündigungen",icon:"📢"},
|
||
{grp:"Moderation"},{id:"moderation",label:"Bans & Warns",icon:"🔨"},{id:"reports",label:"Reports",icon:"🎫"},
|
||
{grp:"System"},{id:"ressourcen",label:"Ressourcen",icon:"🧩"},{id:"logs",label:"Audit-Log",icon:"📜"},{id:"team",label:"Admin-Team",icon:"⭐"},{id:"settings",label:"Einstellungen",icon:"⚙️"}
|
||
];
|
||
function zeitStr(){return String(welt.stunde).padStart(2,"0")+":"+String(welt.minute).padStart(2,"0");}
|
||
function uptimeStr(){const h=Math.floor(server.uptimeMin/60),m=server.uptimeMin%60;return h+"h "+String(m).padStart(2,"0")+"m";}
|
||
function renderTop(){
|
||
const online=players.length;
|
||
$("top").innerHTML=`<div class="srv-chip"><span class="srv-dot"></span>${server.name} · ${server.version}</div>
|
||
<div class="live"><span>Spieler <b>${online}/${server.slots}</b></span><span>TPS <b>${server.tps.toFixed(1)}</b></span><span>CPU <b>${server.cpu}%</b></span><span>Uptime <b>${uptimeStr()}</b></span><span>🕒 <b>${zeitStr()}</b> · ${welt.wetter}</span></div>
|
||
<div class="restart">⟳ Neustart in ${Math.floor(server.neustart/60)}h ${server.neustart%60}m</div>
|
||
<div class="me"><span class="av">OW</span><div><div class="mn">${admin.name}</div><div class="mr">${admin.rolle}</div></div></div>`;
|
||
}
|
||
function renderNav(){
|
||
const offeneReports=reports.filter(r=>r.status!=="geschlossen").length;
|
||
const aktivierbar=staatsprojekte.filter(p=>!p.aktiviert&&p.abgelaufen&&(p.ja/((p.ja+p.nein)||1)>=ZUSTIMMUNG_MIN)).length;
|
||
const badge=(id)=> id==='reports'&&offeneReports?`<span class="badge">${offeneReports}</span>`: id==='staat'&&aktivierbar?`<span class="badge" style="background:var(--green);color:#06231a">${aktivierbar}</span>`:'';
|
||
$("nav").innerHTML=nav.map(it=>it.grp?`<div class="grp">${it.grp}</div>`:
|
||
`<button data-s="${it.id}" class="${it.id===sektion?'active':''}"><span class="ni">${it.icon}</span>${it.label}${badge(it.id)}</button>`).join("");
|
||
$("nav").querySelectorAll("[data-s]").forEach(b=>b.addEventListener("click",()=>{sektion=b.dataset.s;renderNav();renderSektion();}));
|
||
}
|
||
function renderSektion(){clearInterval(mapTimer);(sektionen[sektion]||sektionen.dashboard)();}
|
||
function refresh(){renderTop();renderNav();renderSektion();}
|
||
const sektionen={};
|
||
|
||
/* =====================================================================
|
||
DASHBOARD
|
||
===================================================================== */
|
||
sektionen.dashboard=()=>{
|
||
const online=players.length, hoherPing=players.filter(p=>p.ping>=90).length, offeneReports=reports.filter(r=>r.status!=="geschlossen").length;
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Dashboard</h2><p>Serverüberblick in Echtzeit</p></div></div>
|
||
<div class="grid" style="grid-template-columns:repeat(4,1fr)">
|
||
<div class="card kpi"><div class="l">👥 Spieler online</div><div class="v" id="kpi-online">${online}/${server.slots}</div><div class="d" style="color:var(--green)">${Math.round(online/server.slots*100)}% Auslastung</div></div>
|
||
<div class="card kpi"><div class="l">⚡ Ø TPS</div><div class="v" id="kpi-tps" style="color:${server.tps>18?'var(--green)':'var(--amber)'}">${server.tps.toFixed(1)}</div><div class="d" style="color:var(--muted)">Ziel: 20.0</div></div>
|
||
<div class="card kpi"><div class="l">🖥️ CPU / RAM</div><div class="v"><span id="kpi-cpu">${server.cpu}</span>% / ${server.ram}%</div><div class="d" style="color:var(--muted)">Host-Auslastung</div></div>
|
||
<div class="card kpi"><div class="l">🎫 Offene Reports</div><div class="v" style="color:${offeneReports?'var(--amber)':'var(--green)'}">${offeneReports}</div><div class="d" style="color:var(--muted)">${hoherPing} Spieler mit hohem Ping</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 · letzte 24 h <span class="r">jetzt ${online}</span></div><div class="chart">${areaChart(spieler24h,600,150,'#5b8cff')}</div></div>
|
||
<div class="card"><div class="list-h">Schnellaktionen</div><div class="quick">
|
||
<button class="btn ghost" data-q="broadcast">📢 Ankündigung</button>
|
||
<button class="btn ghost" data-q="wetter">🌦️ Wetter setzen</button>
|
||
<button class="btn ghost" data-q="zeit">🕒 Zeit setzen</button>
|
||
<button class="btn ghost" data-q="healall">❤️ Alle heilen</button>
|
||
<button class="btn ghost" data-q="save">💾 Welt speichern</button>
|
||
<button class="btn red" data-q="restart">⟳ Server-Neustart</button>
|
||
</div></div>
|
||
</div>
|
||
<div class="grid" style="grid-template-columns:1fr 1fr;margin-top:12px">
|
||
<div class="card"><div class="list-h">Ressourcen-Status <span class="r">${ressourcen.filter(r=>r.status==='running').length}/${ressourcen.length} aktiv</span></div>
|
||
${ressourcen.slice(0,5).map(r=>`<div class="kv"><span>${sicher(r.name)}</span><span>${r.status==='running'?`<span class="pill g">läuft</span> ${r.cpu}% CPU`:'<span class="pill r">gestoppt</span>'}</span></div>`).join('')}</div>
|
||
<div class="card"><div class="list-h">Letzte Admin-Aktionen</div>${logs.slice(0,6).map(l=>`<div class="kv"><span>${sicher(l.aktion)}${l.detail?': '+sicher(l.detail):''}</span><span style="color:var(--muted)">${l.zeit.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}</span></div>`).join('')||'<div class="empty">Keine Aktionen.</div>'}</div>
|
||
</div>`;
|
||
$("main").querySelectorAll("[data-q]").forEach(b=>b.addEventListener("click",()=>schnellAktion(b.dataset.q)));
|
||
};
|
||
function schnellAktion(q){
|
||
if(q==="broadcast"){sektion="broadcast";refresh();}
|
||
else if(q==="wetter"||q==="zeit"){sektion="welt";refresh();}
|
||
else if(q==="healall"){log("Alle Spieler geheilt");toast("Alle "+players.length+" Spieler geheilt","good");}
|
||
else if(q==="save"){log("Welt gespeichert");toast("Welt & Spieler gespeichert","good");}
|
||
else if(q==="restart")restartModal();
|
||
}
|
||
function restartModal(){
|
||
modal(`<h3>Server-Neustart</h3><div class="msub">Alle Spieler werden benachrichtigt und die Welt gespeichert.</div>
|
||
<label class="flabel">Countdown</label><select class="input" id="rs-time"><option value="0">Sofort</option><option value="1">1 Minute</option><option value="5" selected>5 Minuten</option><option value="15">15 Minuten</option></select>
|
||
<label class="flabel">Grund (optional)</label><input class="input" id="rs-reason" placeholder="z. B. Update v2.4.2">
|
||
<div class="modal-actions"><button class="btn ghost" id="rs-cancel">Abbrechen</button><button class="btn red" id="rs-ok">Neustart einleiten</button></div>`,true);
|
||
$("rs-cancel").addEventListener("click",closeModal);
|
||
$("rs-ok").addEventListener("click",()=>{const t=$("rs-time").value;log("Neustart geplant",t+" min");closeModal();toast("Neustart in "+t+" min eingeleitet","warn");});
|
||
}
|
||
|
||
/* =====================================================================
|
||
SPIELER
|
||
===================================================================== */
|
||
let spielerSuche="", spielerFilter="alle";
|
||
sektionen.spieler=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Spieler</h2><p>${players.length} online · Verwaltung & Aktionen</p></div>
|
||
<div class="seg" id="sp-filter"><button data-f="alle" class="${spielerFilter==='alle'?'active':''}">Alle</button><button data-f="lspd" class="${spielerFilter==='lspd'?'active':''}">LSPD</button><button data-f="ems" class="${spielerFilter==='ems'?'active':''}">EMS</button><button data-f="flagged" class="${spielerFilter==='flagged'?'active':''}">Auffällig</button></div></div>
|
||
<div class="searchbar" style="margin-bottom:12px"><span>🔎</span><input id="sp-such" placeholder="Name oder Region suchen…" 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();zeichneSpieler();});
|
||
$("sp-filter").querySelectorAll("[data-f]").forEach(b=>b.addEventListener("click",()=>{spielerFilter=b.dataset.f;renderSektion();}));
|
||
zeichneSpieler();
|
||
};
|
||
function spielerListe(){return players.filter(p=>{
|
||
const q=!spielerSuche||(p.name+" "+p.region).toLowerCase().includes(spielerSuche);
|
||
const f=spielerFilter==="alle"||(spielerFilter==="lspd"&&p.fraktion==="LSPD")||(spielerFilter==="ems"&&p.fraktion==="EMS")||(spielerFilter==="flagged"&&(p.warns>0||p.ping>=90||p.muted));
|
||
return q&&f;});}
|
||
function zeichneSpieler(){
|
||
const el=$("sp-body");if(!el)return;
|
||
el.innerHTML=spielerListe().map(p=>`<tr><td class="num" style="color:var(--muted)">#${p.id}</td>
|
||
<td><span class="av" style="background:${avColor(p.name)}">${initialen(p.name)}</span>${sicher(p.name)}</td>
|
||
<td>${sicher(p.job)}${p.fraktion?` <span class="pill b">${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">${playerFlags(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 gefunden.</td></tr>';
|
||
el.querySelectorAll("[data-p]").forEach(b=>b.addEventListener("click",()=>spielerModal(+b.dataset.p)));
|
||
}
|
||
function spielerModal(id){
|
||
const p=player(id);if(!p)return;
|
||
modal(`<h3><span class="av" style="background:${avColor(p.name)};width:30px;height:30px">${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?' · '+p.fraktion:''} · ${sicher(p.region)} · ${p.spielzeit} Std</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>Verwarnungen</span><span>${p.warns}</span></div><div class="kv"><span>Godmode</span><span>${p.god?'an':'aus'}</span></div><div class="kv"><span>Eingefroren</span><span>${p.frozen?'ja':'nein'}</span></div></div></div>
|
||
<div class="actiongrid">
|
||
<div class="ag-h">Teleport & Ansicht</div>
|
||
<button data-a="goto">🧭 Zu Spieler<small>Teleport hin</small></button>
|
||
<button data-a="bring">📥 Herholen<small>zu mir</small></button>
|
||
<button data-a="spectate">👁️ Beobachten<small>Spectate</small></button>
|
||
<button data-a="cam">📹 Live-Kamera<small>WebRTC-View</small></button>
|
||
<div class="ag-h">Zustand</div>
|
||
<button data-a="heal">❤️ Heilen<small>volle HP</small></button>
|
||
<button data-a="revive">✨ Wiederbeleben<small>Revive</small></button>
|
||
<button data-a="armor">🛡️ Rüstung<small>+Armor</small></button>
|
||
<button data-a="freeze" class="${p.frozen?'on':''}">🧊 Einfrieren<small>${p.frozen?'aktiv':'toggle'}</small></button>
|
||
<button data-a="god" class="${p.god?'on':''}">⭐ Godmode<small>${p.god?'aktiv':'toggle'}</small></button>
|
||
<button data-a="mute" class="${p.muted?'on':''}">🔇 Stumm<small>${p.muted?'aktiv':'toggle'}</small></button>
|
||
<div class="ag-h">Geben & Setzen</div>
|
||
<button data-a="money">💵 Geld<small>geben/nehmen</small></button>
|
||
<button data-a="item">🎁 Item/Waffe<small>geben</small></button>
|
||
<button data-a="veh">🚗 Fahrzeug<small>spawnen</small></button>
|
||
<button data-a="job">💼 Job/Fraktion<small>setzen</small></button>
|
||
<div class="ag-h">Moderation</div>
|
||
<button data-a="warn">⚠️ Verwarnen<small>Warn</small></button>
|
||
<button data-a="note">📝 Notiz<small>intern</small></button>
|
||
<button data-a="kick" class="danger">👢 Kicken<small>trennen</small></button>
|
||
<button data-a="ban" class="danger">🔨 Bannen<small>sperren</small></button>
|
||
</div>
|
||
<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)));
|
||
}
|
||
function spielerAktion(p,a){
|
||
const simpel={goto:"Zu "+p.name+" teleportiert",bring:p.name+" hergeholt",spectate:"Beobachte "+p.name,heal:p.name+" geheilt",revive:p.name+" wiederbelebt",armor:p.name+" Rüstung gegeben"};
|
||
if(simpel[a]){log(a+" · "+p.name);toast(simpel[a],"good");return;}
|
||
if(a==="freeze"){p.frozen=!p.frozen;log((p.frozen?"Eingefroren":"Aufgetaut"),p.name);toast(p.name+(p.frozen?" eingefroren":" aufgetaut"));spielerModal(p.id);zeichneSpieler();return;}
|
||
if(a==="god"){p.god=!p.god;log("Godmode "+(p.god?"an":"aus"),p.name);toast(p.name+" Godmode "+(p.god?"an":"aus"));spielerModal(p.id);zeichneSpieler();return;}
|
||
if(a==="mute"){p.muted=!p.muted;log("Mute "+(p.muted?"an":"aus"),p.name);toast(p.name+(p.muted?" stummgeschaltet":" entstummt"));spielerModal(p.id);zeichneSpieler();return;}
|
||
if(a==="cam")return einzelKamera(p);
|
||
if(a==="money")return geldModal(p);
|
||
if(a==="item")return itemModal(p);
|
||
if(a==="veh")return vehGebenModal(p);
|
||
if(a==="job")return jobModal(p);
|
||
if(a==="warn")return warnModal(p);
|
||
if(a==="note")return notizModal(p);
|
||
if(a==="kick")return kickModal(p);
|
||
if(a==="ban")return banModal(p);
|
||
}
|
||
function geldModal(p){
|
||
modal(`<h3>Geld · ${sicher(p.name)}</h3><div class="msub">Bargeld ${euro(p.cash)} · Bank ${euro(p.bank)}</div>
|
||
<label class="flabel">Konto</label><select class="input" id="g-konto"><option value="cash">Bargeld</option><option value="bank" selected>Bank</option></select>
|
||
<label class="flabel">Betrag (negativ = entfernen)</label><input class="input" id="g-betrag" type="number" placeholder="z. B. 5000">
|
||
<div class="modal-actions"><button class="btn ghost" id="g-cancel">Abbrechen</button><button class="btn primary" id="g-ok">Anwenden</button></div>`,true);
|
||
$("g-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("g-ok").addEventListener("click",()=>{const b=parseInt($("g-betrag").value,10);if(!b){toast("Betrag angeben","warn");return;}const k=$("g-konto").value;p[k]=Math.max(0,p[k]+b);log("Geld "+(b>0?"+":"")+b,p.name+" ("+k+")");toast((b>0?"+":"")+euro(Math.abs(b)).replace('-','')+" "+(b>0?"gutgeschrieben":"entfernt"),"good");spielerModal(p.id);zeichneSpieler();});
|
||
}
|
||
function itemModal(p){
|
||
const items=["Wasserflasche","Sandwich","Verband","Reparaturkit","Dietrich","Pistole P99","9mm Munition","Funkgerät","Smartphone"];
|
||
modal(`<h3>Item / Waffe geben · ${sicher(p.name)}</h3><div class="msub">Fügt das Item dem Inventar hinzu</div>
|
||
<label class="flabel">Item</label><select class="input" id="i-item">${items.map(i=>`<option>${i}</option>`).join('')}</select>
|
||
<label class="flabel">Menge</label><input class="input" id="i-menge" type="number" value="1">
|
||
<div class="modal-actions"><button class="btn ghost" id="i-cancel">Abbrechen</button><button class="btn primary" id="i-ok">Geben</button></div>`,true);
|
||
$("i-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("i-ok").addEventListener("click",()=>{const it=$("i-item").value,m=parseInt($("i-menge").value,10)||1;log("Item gegeben",p.name+": "+m+"× "+it);closeModal();toast(m+"× "+it+" an "+p.name,"good");});
|
||
}
|
||
function vehGebenModal(p){
|
||
modal(`<h3>Fahrzeug spawnen · ${sicher(p.name)}</h3><div class="msub">Erstellt ein Fahrzeug für den Spieler</div>
|
||
<label class="flabel">Modell</label><select class="input" id="v-model">${fahrzeugModelle.map(m=>`<option>${m}</option>`).join('')}</select>
|
||
<div class="modal-actions"><button class="btn ghost" id="v-cancel">Abbrechen</button><button class="btn primary" id="v-ok">Spawnen</button></div>`,true);
|
||
$("v-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("v-ok").addEventListener("click",()=>{const m=$("v-model").value;const plate="AD-"+Math.floor(1000+Math.random()*9000);weltFahrzeuge.unshift({model:m,plate,owner:p.name,zustand:100});log("Fahrzeug gespawnt",m+" für "+p.name);closeModal();toast(m+" ("+plate+") für "+p.name+" gespawnt","good");});
|
||
}
|
||
function jobModal(p){
|
||
modal(`<h3>Job / Fraktion · ${sicher(p.name)}</h3>
|
||
<label class="flabel">Job</label><select class="input" id="j-job">${jobs.map(j=>`<option ${p.job===j?'selected':''}>${j}</option>`).join('')}</select>
|
||
<label class="flabel">Fraktion</label><select class="input" id="j-frak"><option value="">— keine —</option>${fraktionen.map(f=>`<option ${p.fraktion===f.name?'selected':''}>${f.name}</option>`).join('')}</select>
|
||
<div class="modal-actions"><button class="btn ghost" id="j-cancel">Abbrechen</button><button class="btn primary" id="j-ok">Setzen</button></div>`,true);
|
||
$("j-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("j-ok").addEventListener("click",()=>{p.job=$("j-job").value;p.fraktion=$("j-frak").value||null;log("Job/Fraktion gesetzt",p.name+": "+p.job+(p.fraktion?" / "+p.fraktion:""));toast(p.name+" → "+p.job,"good");spielerModal(p.id);zeichneSpieler();});
|
||
}
|
||
function warnModal(p){
|
||
modal(`<h3>Verwarnung · ${sicher(p.name)}</h3><div class="msub">Bisher ${p.warns} Verwarnung(en)</div>
|
||
<label class="flabel">Grund</label><input class="input" id="wn-grund" placeholder="z. B. Fail RP">
|
||
<div class="modal-actions"><button class="btn ghost" id="wn-cancel">Abbrechen</button><button class="btn amber" id="wn-ok">Verwarnen</button></div>`,true);
|
||
$("wn-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("wn-ok").addEventListener("click",()=>{const g=$("wn-grund").value.trim()||"Regelverstoß";p.warns++;warns.unshift({name:p.name,grund:g,admin:admin.name,datum:new Date().toLocaleDateString('de-DE')});log("Verwarnung",p.name+": "+g);toast(p.name+" verwarnt","warn");spielerModal(p.id);zeichneSpieler();});
|
||
}
|
||
function notizModal(p){
|
||
modal(`<h3>Interne Notiz · ${sicher(p.name)}</h3><textarea class="input" id="nt-text" placeholder="Notiz zum Spieler…"></textarea>
|
||
<div class="modal-actions"><button class="btn ghost" id="nt-cancel">Abbrechen</button><button class="btn primary" id="nt-ok">Speichern</button></div>`,true);
|
||
$("nt-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("nt-ok").addEventListener("click",()=>{const t=$("nt-text").value.trim();if(!t){toast("Text eingeben","warn");return;}log("Notiz",p.name+": "+t);closeModal();toast("Notiz gespeichert","good");});
|
||
}
|
||
function kickModal(p){
|
||
modal(`<h3>Spieler kicken · ${sicher(p.name)}</h3><label class="flabel">Grund</label><input class="input" id="k-grund" placeholder="z. B. AFK / Regelbruch">
|
||
<div class="modal-actions"><button class="btn ghost" id="k-cancel">Abbrechen</button><button class="btn red" id="k-ok">Kicken</button></div>`,true);
|
||
$("k-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("k-ok").addEventListener("click",()=>{const g=$("k-grund").value.trim()||"Kein Grund";players=players.filter(x=>x.id!==p.id);log("Kick",p.name+": "+g);closeModal();toast(p.name+" gekickt","warn");refresh();});
|
||
}
|
||
function banModal(p){
|
||
modal(`<h3>Spieler bannen · ${sicher(p.name)}</h3><div class="msub">Spieler wird getrennt und gesperrt.</div>
|
||
<label class="flabel">Grund</label><input class="input" id="b-grund" placeholder="z. B. Cheating">
|
||
<label class="flabel">Dauer</label><select class="input" id="b-dauer"><option>1 Tag</option><option>3 Tage</option><option selected>7 Tage</option><option>30 Tage</option><option>permanent</option></select>
|
||
<div class="modal-actions"><button class="btn ghost" id="b-cancel">Abbrechen</button><button class="btn red" id="b-ok">Bannen</button></div>`,true);
|
||
$("b-cancel").addEventListener("click",()=>spielerModal(p.id));
|
||
$("b-ok").addEventListener("click",()=>{const g=$("b-grund").value.trim()||"Regelbruch",d=$("b-dauer").value;players=players.filter(x=>x.id!==p.id);bans.unshift({name:p.name,grund:g,admin:admin.name,dauer:d,datum:new Date().toLocaleDateString('de-DE')});log("Ban",p.name+": "+g+" ("+d+")");closeModal();toast(p.name+" gebannt ("+d+")","warn");refresh();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
FAHRZEUGE
|
||
===================================================================== */
|
||
sektionen.fahrzeuge=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Fahrzeuge</h2><p>${weltFahrzeuge.length} in der Welt</p></div>
|
||
<div style="display:flex;gap:8px"><button class="btn ghost" id="fz-clean">🧹 Aufräumen</button><button class="btn primary" id="fz-spawn">+ Fahrzeug spawnen</button></div></div>
|
||
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Fahrzeug</th><th>Kennzeichen</th><th>Besitzer</th><th>Zustand</th><th></th></tr></thead><tbody>
|
||
${weltFahrzeuge.map((v,i)=>`<tr><td><span class="veh-thumb">${fahrzeugIcon(v.typ,v.farbe)}</span>${sicher(v.model)}${v.tuning&&v.tuning.turbo?' <span class="pill v">Turbo</span>':''}</td><td class="mono">${sicher(v.plate)}</td><td>${sicher(v.owner||'—')}</td>
|
||
<td><span class="barmini"><i style="width:${v.zustand}%;background:${v.zustand>70?'var(--green)':v.zustand>40?'var(--amber)':'var(--red)'}"></i></span> ${v.zustand}%</td>
|
||
<td class="num"><button class="btn primary sm" data-tune="${i}">Bearbeiten</button> <button class="btn ghost sm" data-repair="${i}">Reparieren</button> <button class="btn ghost sm" data-tp="${i}">Herholen</button> <button class="btn red sm" data-del="${i}">Löschen</button></td></tr>`).join('')||'<tr><td colspan="5" class="empty">Keine Fahrzeuge.</td></tr>'}
|
||
</tbody></table></div></div>`;
|
||
$("fz-spawn").addEventListener("click",spawnVehModal);
|
||
$("fz-clean").addEventListener("click",()=>{const n=weltFahrzeuge.length;weltFahrzeuge=[];log("Fahrzeuge aufgeräumt",n+" entfernt");toast(n+" Fahrzeuge entfernt","good");renderSektion();});
|
||
$("main").querySelectorAll("[data-tune]").forEach(b=>b.addEventListener("click",()=>tuningModal(+b.dataset.tune)));
|
||
$("main").querySelectorAll("[data-repair]").forEach(b=>b.addEventListener("click",()=>{weltFahrzeuge[+b.dataset.repair].zustand=100;log("Fahrzeug repariert",weltFahrzeuge[+b.dataset.repair].plate);toast("Repariert","good");renderSektion();}));
|
||
$("main").querySelectorAll("[data-tp]").forEach(b=>b.addEventListener("click",()=>{toast(weltFahrzeuge[+b.dataset.tp].model+" hergeholt","good");log("Fahrzeug hergeholt",weltFahrzeuge[+b.dataset.tp].plate);}));
|
||
$("main").querySelectorAll("[data-del]").forEach(b=>b.addEventListener("click",()=>{const v=weltFahrzeuge.splice(+b.dataset.del,1)[0];log("Fahrzeug gelöscht",v.plate);toast(v.model+" gelöscht","warn");renderSektion();}));
|
||
};
|
||
/* Bild-Spawner: Kategorie wählen, Fahrzeug anklicken */
|
||
function spawnVehModal(){
|
||
let katIdx=0, such="";
|
||
modal(`<h3>Fahrzeug spawnen</h3><div class="msub">Kategorie wählen und auf ein Fahrzeug klicken</div>
|
||
<label class="flabel">Besitzer</label><select class="input" id="sv-owner"><option value="">— Admin —</option>${players.map(p=>`<option>${sicher(p.name)}</option>`).join('')}</select>
|
||
<div class="seg" id="sv-tabs" style="margin:12px 0 8px;flex-wrap:wrap;display:flex">${fahrzeugKatalog.map((k,i)=>`<button data-k="${i}" class="${i===0?'active':''}">${k.kat}</button>`).join('')}</div>
|
||
<div class="searchbar" style="margin-bottom:10px"><span>🔎</span><input id="sv-such" placeholder="Modell suchen (alle Kategorien)…"></div>
|
||
<div id="sv-grid" class="vehgrid"></div>
|
||
<div class="modal-actions" style="grid-template-columns:1fr"><button class="btn ghost" id="sv-close">Schließen</button></div>`);
|
||
const zeichne=()=>{
|
||
let items=[];
|
||
if(such)fahrzeugKatalog.forEach(k=>k.items.forEach(n=>{if(n.toLowerCase().includes(such))items.push({name:n,typ:k.typ});}));
|
||
else {const k=fahrzeugKatalog[katIdx];items=k.items.map(n=>({name:n,typ:k.typ}));}
|
||
$("sv-grid").innerHTML=items.map(it=>`<button class="vehcard" data-spawn="${sicher(it.name)}" data-typ="${it.typ}"><div class="vehimg">${fahrzeugIcon(it.typ,vehColor(it.name))}</div><div class="vehname">${sicher(it.name)}</div></button>`).join('')||'<div class="empty" style="grid-column:1/-1">Kein Treffer.</div>';
|
||
$("sv-grid").querySelectorAll("[data-spawn]").forEach(b=>b.addEventListener("click",()=>{
|
||
const o=$("sv-owner").value||"Admin", m=b.dataset.spawn, plate="AD-"+Math.floor(1000+Math.random()*9000);
|
||
weltFahrzeuge.unshift({model:m,plate,owner:o,zustand:100,typ:b.dataset.typ,farbe:vehColor(m),tuning:{motor:0,bremsen:0,getriebe:0,turbo:false,panzerung:0}});
|
||
log("Fahrzeug gespawnt",m+" für "+o);toast(m+" ("+plate+") für "+o+" gespawnt","good");}));
|
||
};
|
||
$("sv-tabs").querySelectorAll("[data-k]").forEach(b=>b.addEventListener("click",()=>{katIdx=+b.dataset.k;such="";$("sv-such").value="";$("sv-tabs").querySelectorAll("[data-k]").forEach(x=>x.classList.remove("active"));b.classList.add("active");zeichne();}));
|
||
$("sv-such").addEventListener("input",e=>{such=e.target.value.trim().toLowerCase();zeichne();});
|
||
$("sv-close").addEventListener("click",()=>{closeModal();renderSektion();});
|
||
zeichne();
|
||
}
|
||
/* Fahrzeug bearbeiten: Leistung, Einbauten, Lack */
|
||
function tuningModal(i){const v=weltFahrzeuge[i],t=v.tuning;let farbe=v.farbe;
|
||
const stufe=(id,label)=>`<div><label class="flabel">${label}</label><select class="input" data-tn="${id}">${[0,1,2,3,4].map(s=>`<option value="${s}" ${t[id]===s?'selected':''}>${s===0?'Serie':'Stufe '+s}</option>`).join('')}</select></div>`;
|
||
const swatches=["#c0392b","#2f7bbf","#20232a","#e6e8ee","#2f6b4a","#e0a020","#7c5cf6","#c79a5a","#f0506e","#34d39a"];
|
||
modal(`<h3>🔧 Fahrzeug bearbeiten · ${sicher(v.model)}</h3><div class="msub">Leistung, Einbauten & Optik · ${sicher(v.plate)}</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 14px">${stufe('motor','Motor')}${stufe('getriebe','Getriebe')}${stufe('bremsen','Bremsen')}${stufe('panzerung','Panzerung')}</div>
|
||
<label class="chk" style="margin-top:10px;cursor:pointer"><input type="checkbox" id="tn-turbo" ${t.turbo?'checked':''} style="width:16px;height:16px;accent-color:var(--accent)"><div class="grow" style="font-weight:700;font-size:13px">Turbo-Umbau</div></label>
|
||
<label class="flabel">Lackfarbe</label><div class="swatches" id="tn-farbe" style="display:flex;flex-wrap:wrap;gap:8px">${swatches.map(c=>`<div class="sw2" data-c="${c}" style="background:${c};border:2px solid ${v.farbe===c?'#fff':'transparent'};box-shadow:${v.farbe===c?'0 0 0 2px var(--accent)':'none'}"></div>`).join('')}</div>
|
||
<div class="modal-actions"><button class="btn ghost" id="tn-cancel">Abbrechen</button><button class="btn primary" id="tn-ok">Übernehmen</button></div>`);
|
||
$("tn-farbe").querySelectorAll("[data-c]").forEach(el=>el.addEventListener("click",()=>{farbe=el.dataset.c;$("tn-farbe").querySelectorAll("[data-c]").forEach(x=>{x.style.borderColor='transparent';x.style.boxShadow='none';});el.style.borderColor='#fff';el.style.boxShadow='0 0 0 2px var(--accent)';}));
|
||
$("tn-cancel").addEventListener("click",closeModal);
|
||
$("tn-ok").addEventListener("click",()=>{$("overlay").querySelectorAll("[data-tn]").forEach(s=>{t[s.dataset.tn]=+s.value;});t.turbo=$("tn-turbo").checked;v.farbe=farbe;log("Fahrzeug bearbeitet",v.model+" ("+v.plate+")");closeModal();toast("Fahrzeug bearbeitet","good");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
IMMOBILIEN
|
||
===================================================================== */
|
||
sektionen.immobilien=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Immobilien</h2><p>${immobilien.length} Objekte · ${immobilien.filter(i=>i.besitzer).length} vergeben</p></div></div>
|
||
<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>
|
||
${immobilien.map((im,i)=>`<tr><td>🏠 ${sicher(im.name)}</td><td style="color:var(--muted)">${sicher(im.ort)}</td>
|
||
<td>${im.besitzer?sicher(im.besitzer):'<span class="pill m">frei</span>'}</td><td class="num">${euro(im.preis)}</td>
|
||
<td class="num"><button class="btn ghost sm" data-assign="${i}">Besitzer setzen</button>${im.besitzer?` <button class="btn red sm" data-evict="${i}">Räumen</button>`:''}</td></tr>`).join('')}
|
||
</tbody></table></div></div>`;
|
||
$("main").querySelectorAll("[data-assign]").forEach(b=>b.addEventListener("click",()=>immoAssign(+b.dataset.assign)));
|
||
$("main").querySelectorAll("[data-evict]").forEach(b=>b.addEventListener("click",()=>{const im=immobilien[+b.dataset.evict];log("Immobilie geräumt",im.name);im.besitzer=null;toast(im.name+" geräumt","warn");renderSektion();}));
|
||
};
|
||
function immoAssign(i){const im=immobilien[i];
|
||
modal(`<h3>Besitzer setzen · ${sicher(im.name)}</h3><label class="flabel">Neuer Besitzer</label><select class="input" id="im-owner"><option value="">— frei —</option>${players.map(p=>`<option ${im.besitzer===p.name?'selected':''}>${sicher(p.name)}</option>`).join('')}</select>
|
||
<div class="modal-actions"><button class="btn ghost" id="im-cancel">Abbrechen</button><button class="btn primary" id="im-ok">Setzen</button></div>`,true);
|
||
$("im-cancel").addEventListener("click",closeModal);
|
||
$("im-ok").addEventListener("click",()=>{im.besitzer=$("im-owner").value||null;log("Immobilie zugewiesen",im.name+" → "+(im.besitzer||"frei"));closeModal();toast("Besitzer aktualisiert","good");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
FRAKTIONEN
|
||
===================================================================== */
|
||
sektionen.fraktionen=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Fraktionen</h2><p>${fraktionen.length} Fraktionen · ${fraktionen.reduce((s,f)=>s+f.mitglieder,0)} Mitglieder</p></div><button class="btn primary" id="fr-neu">+ Neue Fraktion</button></div>
|
||
<div class="grid" style="grid-template-columns:repeat(2,1fr)">${fraktionen.map((f,i)=>`<div class="card"><div style="display:flex;align-items:center;gap:10px"><span class="av" style="background:${f.farbe}">${initialen(f.name)}</span><div class="grow" style="flex:1"><div style="font-weight:800">${sicher(f.name)}</div><div style="font-size:12px;color:var(--muted)">${f.mitglieder} Mitglieder</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"><button class="btn ghost sm" data-gehalt="${i}">Gehalt anpassen</button><button class="btn ghost sm" data-members="${i}">Mitglieder</button><button class="btn red sm" data-delfrak="${i}">Löschen</button></div></div>`).join('')}</div>
|
||
<div class="card" style="margin-top:12px;font-size:12px;color:var(--muted)">💡 Das <b style="color:var(--fg)">Gehalt</b> ist der Betrag, der pro Stunde geleisteter Arbeit im <b style="color:var(--fg)">Payday</b> an jedes Mitglied ausgezahlt wird. Ein dediziertes Fraktions-/Gang-System (Ränge, Kassen, Territorien) folgt separat — diese Fraktionen sind dort bereits enthalten.</div>`;
|
||
$("fr-neu").addEventListener("click",fraktionModal);
|
||
$("main").querySelectorAll("[data-gehalt]").forEach(b=>b.addEventListener("click",()=>gehaltModal(+b.dataset.gehalt)));
|
||
$("main").querySelectorAll("[data-members]").forEach(b=>b.addEventListener("click",()=>{const f=fraktionen[+b.dataset.members],list=players.filter(p=>p.fraktion===f.name);
|
||
modal(`<h3>Mitglieder · ${sicher(f.name)}</h3><div class="msub">Online: ${list.length} von ${f.mitglieder}</div>${list.map(p=>`<div class="kv"><span>${sicher(p.name)}</span><span>${sicher(p.job)}</span></div>`).join('')||'<div class="empty">Niemand online.</div>'}<div class="modal-actions" style="grid-template-columns:1fr"><button class="btn ghost" id="fm-c">Schließen</button></div>`);
|
||
$("fm-c").addEventListener("click",closeModal);}));
|
||
$("main").querySelectorAll("[data-delfrak]").forEach(b=>b.addEventListener("click",()=>{const f=fraktionen[+b.dataset.delfrak];
|
||
modal(`<h3>Fraktion löschen?</h3><p style="color:var(--muted);font-size:13px">„${sicher(f.name)}“ wird entfernt. Mitglieder verlieren ihre Zugehörigkeit.</p><div class="modal-actions"><button class="btn ghost" id="df-c">Abbrechen</button><button class="btn red" id="df-ok">Löschen</button></div>`,true);
|
||
$("df-c").addEventListener("click",closeModal);$("df-ok").addEventListener("click",()=>{players.forEach(p=>{if(p.fraktion===f.name)p.fraktion=null;});fraktionen=fraktionen.filter(x=>x!==f);log("Fraktion gelöscht",f.name);closeModal();toast("Fraktion gelöscht","warn");renderSektion();});}));
|
||
};
|
||
function gehaltModal(i){const f=fraktionen[i];
|
||
modal(`<h3>Gehalt · ${sicher(f.name)}</h3><div class="msub">Stündliche Payday-Auszahlung pro Mitglied</div><label class="flabel">Gehalt (€ pro Stunde)</label><input class="input" id="fg" type="number" value="${f.gehalt}">
|
||
<div class="modal-actions"><button class="btn ghost" id="fg-c">Abbrechen</button><button class="btn primary" id="fg-ok">Speichern</button></div>`,true);
|
||
$("fg-c").addEventListener("click",closeModal);$("fg-ok").addEventListener("click",()=>{f.gehalt=Math.max(0,parseInt($("fg").value,10)||0);log("Fraktionsgehalt",f.name+" → "+euro(f.gehalt)+"/h");closeModal();toast("Gehalt: "+euro(f.gehalt)+"/h","good");renderSektion();});
|
||
}
|
||
function fraktionModal(){const swatches=["#4aa8ff","#f0506e","#7c5cf6","#f5b23d","#34d39a","#e0503f","#c96fd6","#22c1d4"];let farbe=swatches[0];
|
||
modal(`<h3>Neue Fraktion</h3><label class="flabel">Name</label><input class="input" id="nf-name" placeholder="z. B. Marabunta">
|
||
<label class="flabel">Farbe</label><div class="swatches" id="nf-farbe" style="display:flex;gap:8px;flex-wrap:wrap">${swatches.map((c,i)=>`<div class="sw2" data-c="${c}" style="background:${c};border:2px solid ${i===0?'#fff':'transparent'}"></div>`).join('')}</div>
|
||
<label class="flabel">Gehalt (€ / Stunde)</label><input class="input" id="nf-gehalt" type="number" value="0">
|
||
<div class="modal-actions"><button class="btn ghost" id="nf-c">Abbrechen</button><button class="btn primary" id="nf-ok">Erstellen</button></div>`,true);
|
||
$("nf-farbe").querySelectorAll("[data-c]").forEach(el=>el.addEventListener("click",()=>{farbe=el.dataset.c;$("nf-farbe").querySelectorAll("[data-c]").forEach(x=>x.style.borderColor='transparent');el.style.borderColor='#fff';}));
|
||
$("nf-c").addEventListener("click",closeModal);
|
||
$("nf-ok").addEventListener("click",()=>{const n=$("nf-name").value.trim();if(!n){toast("Name angeben","warn");return;}fraktionen.push({name:n,mitglieder:0,gehalt:parseInt($("nf-gehalt").value,10)||0,farbe});log("Fraktion erstellt",n);closeModal();toast("Fraktion „"+n+"“ erstellt","good");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
WIRTSCHAFT
|
||
===================================================================== */
|
||
sektionen.wirtschaft=()=>{
|
||
const vermoegen=players.reduce((s,p)=>s+p.cash+p.bank,0), avg=Math.round(vermoegen/players.length);
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Wirtschaft</h2><p>Geldmenge, Staatskasse & Steuern</p></div></div>
|
||
<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(economy.umlauf)}</div><div class="d" style="color:var(--muted)">gesamt</div></div>
|
||
<div class="card kpi"><div class="l">🏛️ Staatskasse</div><div class="v">${kompakt(economy.staatskasse)}</div><div class="d" style="color:var(--green)">stabil</div></div>
|
||
<div class="card kpi"><div class="l">👤 Ø Vermögen</div><div class="v">${kompakt(avg)}</div><div class="d" style="color:var(--muted)">pro Spieler online</div></div>
|
||
<div class="card kpi"><div class="l">📊 Steuersatz</div><div class="v">${economy.steuer}%</div><div class="d" style="color:var(--muted)">Einkommen</div></div></div>
|
||
<div class="grid" style="grid-template-columns:1.6fr 1fr;margin-top:12px">
|
||
<div class="card"><div class="list-h">Geldmenge · Verlauf</div><div class="chart">${areaChart(geldVerlauf,600,150,'#34d39a')}</div></div>
|
||
<div class="card"><div class="list-h">Aktionen</div><div style="display:flex;flex-direction:column;gap:8px">
|
||
<button class="btn ghost" id="e-all">💸 Geld an alle Spieler</button>
|
||
<button class="btn ghost" id="e-kasse">🏛️ Staatskasse anpassen</button>
|
||
<button class="btn ghost" id="e-steuer">📊 Steuersatz ändern</button></div></div></div>`;
|
||
$("e-all").addEventListener("click",()=>{modal(`<h3>Geld an alle</h3><label class="flabel">Betrag pro Spieler (Bank)</label><input class="input" id="ea" type="number" placeholder="z. B. 1000"><div class="modal-actions"><button class="btn ghost" id="ea-c">Abbrechen</button><button class="btn primary" id="ea-ok">Auszahlen</button></div>`,true);
|
||
$("ea-c").addEventListener("click",closeModal);$("ea-ok").addEventListener("click",()=>{const b=parseInt($("ea").value,10);if(!b){toast("Betrag angeben","warn");return;}players.forEach(p=>p.bank+=b);log("Geld an alle",euro(b)+" × "+players.length);closeModal();toast(euro(b)+" an "+players.length+" Spieler","good");renderSektion();});});
|
||
$("e-kasse").addEventListener("click",()=>{modal(`<h3>Staatskasse</h3><label class="flabel">Neuer Stand (€)</label><input class="input" id="ek" type="number" value="${economy.staatskasse}"><div class="modal-actions"><button class="btn ghost" id="ek-c">Abbrechen</button><button class="btn primary" id="ek-ok">Speichern</button></div>`,true);
|
||
$("ek-c").addEventListener("click",closeModal);$("ek-ok").addEventListener("click",()=>{economy.staatskasse=parseInt($("ek").value,10)||0;log("Staatskasse gesetzt",euro(economy.staatskasse));closeModal();toast("Staatskasse aktualisiert","good");renderSektion();});});
|
||
$("e-steuer").addEventListener("click",()=>{modal(`<h3>Steuersatz</h3><label class="flabel">Prozent</label><input class="input" id="es" type="number" value="${economy.steuer}"><div class="modal-actions"><button class="btn ghost" id="es-c">Abbrechen</button><button class="btn primary" id="es-ok">Speichern</button></div>`,true);
|
||
$("es-c").addEventListener("click",closeModal);$("es-ok").addEventListener("click",()=>{economy.steuer=Math.max(0,Math.min(100,parseInt($("es").value,10)||0));log("Steuersatz",economy.steuer+"%");closeModal();toast("Steuersatz: "+economy.steuer+"%","good");renderSektion();});});
|
||
};
|
||
|
||
/* =====================================================================
|
||
WELTSTEUERUNG
|
||
===================================================================== */
|
||
sektionen.welt=()=>{
|
||
const wetterOpt=["Klar","Bewölkt","Regen","Gewitter","Nebel","Schnee","Sturm"];
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Weltsteuerung</h2><p>Zeit, Wetter & Umgebung</p></div></div>
|
||
<div class="grid" style="grid-template-columns:1fr 1fr">
|
||
<div class="card"><div class="list-h">🕒 Zeit <span class="r">${zeitStr()} Uhr</span></div>
|
||
<input type="range" min="0" max="23" value="${welt.stunde}" 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 ${welt.zeitFix?'green':'ghost'}" id="w-zeitfix" style="width:100%;margin-top:10px">${welt.zeitFix?'🔒 Zeit eingefroren':'Zeit einfrieren'}</button></div>
|
||
<div class="card"><div class="list-h">🌦️ Wetter <span class="r">${welt.wetter}</span></div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px">${wetterOpt.map(w=>`<button class="btn ${welt.wetter===w?'primary':'ghost'} sm" data-w="${w}">${w}</button>`).join('')}</div>
|
||
<button class="btn ${welt.wetterFix?'green':'ghost'}" id="w-wfix" style="width:100%;margin-top:10px">${welt.wetterFix?'🔒 Wetter eingefroren':'Wetter einfrieren'}</button>
|
||
<button class="btn ${welt.blackout?'amber':'ghost'}" id="w-blackout" style="width:100%;margin-top:8px">${welt.blackout?'💡 Blackout aktiv':'🌑 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">Fahrzeuge entfernen</button><button class="btn ghost" data-clean="ped">NPCs entfernen</button><button class="btn ghost" data-clean="obj">Objekte entfernen</button><button class="btn ghost" data-clean="ground">Leichen aufräumen</button></div></div>`;
|
||
$("w-stunde").addEventListener("input",e=>{welt.stunde=+e.target.value;log("Zeit gesetzt",zeitStr());renderTop();renderSektion();});
|
||
$("main").querySelectorAll("[data-h]").forEach(b=>b.addEventListener("click",()=>{welt.stunde=+b.dataset.h;log("Zeit gesetzt",zeitStr());renderTop();renderSektion();toast("Zeit: "+zeitStr()+" Uhr");}));
|
||
$("w-zeitfix").addEventListener("click",()=>{welt.zeitFix=!welt.zeitFix;log("Zeit "+(welt.zeitFix?"eingefroren":"freigegeben"));renderSektion();toast(welt.zeitFix?"Zeit eingefroren":"Zeit läuft");});
|
||
$("main").querySelectorAll("[data-w]").forEach(b=>b.addEventListener("click",()=>{welt.wetter=b.dataset.w;log("Wetter gesetzt",welt.wetter);renderTop();renderSektion();toast("Wetter: "+welt.wetter,"good");}));
|
||
$("w-wfix").addEventListener("click",()=>{welt.wetterFix=!welt.wetterFix;log("Wetter "+(welt.wetterFix?"eingefroren":"freigegeben"));renderSektion();});
|
||
$("w-blackout").addEventListener("click",()=>{welt.blackout=!welt.blackout;log("Blackout "+(welt.blackout?"an":"aus"));renderSektion();toast(welt.blackout?"Blackout aktiviert":"Blackout deaktiviert","warn");});
|
||
$("main").querySelectorAll("[data-clean]").forEach(b=>b.addEventListener("click",()=>{const map={veh:"Fahrzeuge",ped:"NPCs",obj:"Objekte",ground:"Leichen"};log("Bereinigt",map[b.dataset.clean]);toast(map[b.dataset.clean]+" entfernt","good");}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
ANKÜNDIGUNGEN
|
||
===================================================================== */
|
||
const broadcasts=[];
|
||
sektionen.broadcast=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Ankündigungen</h2><p>Nachricht an alle Spieler senden</p></div></div>
|
||
<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><option>Neustart</option></select>
|
||
<label class="flabel">Nachricht</label><textarea class="input" id="bc-msg" placeholder="Nachricht 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>${broadcasts.length?broadcasts.map(b=>`<div class="kv"><span><span class="pill ${b.typ==='Warnung'?'r':b.typ==='Event'?'v':'b'}">${b.typ}</span> ${sicher(b.msg)}</span><span style="color:var(--muted)">${b.zeit}</span></div>`).join(''):'<div class="empty">Noch keine Ankündigungen.</div>'}</div></div>`;
|
||
$("bc-send").addEventListener("click",()=>{const m=$("bc-msg").value.trim(),t=$("bc-typ").value;if(!m){toast("Nachricht eingeben","warn");return;}broadcasts.unshift({typ:t,msg:m,zeit:new Date().toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})});log("Ankündigung",t+": "+m);toast("An alle gesendet","good");renderSektion();});
|
||
};
|
||
|
||
/* =====================================================================
|
||
MODERATION
|
||
===================================================================== */
|
||
sektionen.moderation=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Bans & Verwarnungen</h2><p>${bans.length} Bans · ${warns.length} Warns</p></div><button class="btn red" id="mod-ban">+ Manueller Ban</button></div>
|
||
<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start">
|
||
<div class="card"><div class="list-h">🔨 Aktive Bans</div>${bans.map((b,i)=>`<div class="row" style="margin-bottom:6px;padding:9px 11px;background:var(--slot)"><div class="grow"><div style="font-weight:700">${sicher(b.name)}</div><div style="font-size:11.5px;color:var(--muted)">${sicher(b.grund)} · ${b.dauer} · ${b.admin}</div></div><button class="btn ghost sm" data-unban="${i}">Entbannen</button></div>`).join('')||'<div class="empty">Keine Bans.</div>'}</div>
|
||
<div class="card"><div class="list-h">⚠️ Verwarnungen</div>${warns.map((w,i)=>`<div class="row" style="margin-bottom:6px;padding:9px 11px;background:var(--slot)"><div class="grow"><div style="font-weight:700">${sicher(w.name)}</div><div style="font-size:11.5px;color:var(--muted)">${sicher(w.grund)} · ${w.admin} · ${w.datum}</div></div><button class="btn ghost sm" data-delwarn="${i}">Löschen</button></div>`).join('')||'<div class="empty">Keine Verwarnungen.</div>'}</div></div>`;
|
||
$("mod-ban").addEventListener("click",()=>{modal(`<h3>Manueller Ban</h3><label class="flabel">Name / Identifier</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",()=>{const n=$("mb-name").value.trim();if(!n){toast("Name angeben","warn");return;}bans.unshift({name:n,grund:$("mb-grund").value.trim()||"—",admin:admin.name,dauer:$("mb-dauer").value,datum:new Date().toLocaleDateString('de-DE')});log("Manueller Ban",n);closeModal();toast(n+" gebannt","warn");renderSektion();});});
|
||
$("main").querySelectorAll("[data-unban]").forEach(b=>b.addEventListener("click",()=>{const x=bans.splice(+b.dataset.unban,1)[0];log("Entbannt",x.name);toast(x.name+" entbannt","good");renderSektion();}));
|
||
$("main").querySelectorAll("[data-delwarn]").forEach(b=>b.addEventListener("click",()=>{const x=warns.splice(+b.dataset.delwarn,1)[0];log("Verwarnung gelöscht",x.name);toast("Verwarnung gelöscht");renderSektion();}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
REPORTS
|
||
===================================================================== */
|
||
sektionen.reports=()=>{
|
||
const prioP={1:['Priorität 1','r','#f0506e'],2:['Priorität 2','a','#f5b23d'],3:['Priorität 3','b','#4aa8ff']};
|
||
const statusP={offen:['offen','a'],'in Bearbeitung':['in Bearbeitung','b'],geschlossen:['geschlossen','g']};
|
||
const offen=reports.filter(r=>r.status!=='geschlossen').length;
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Reports / Support</h2><p>${offen} offen · ${reports.length} gesamt</p></div></div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">${reports.map((r,i)=>{const pp=prioP[r.prio],sp=statusP[r.status]||['?','m'];
|
||
return `<div class="card" style="display:flex;align-items:center;gap:16px;padding:14px 18px;border-left:3px solid ${pp[2]}">
|
||
<div class="grow" style="flex:1;min-width:0">
|
||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span style="font-family:ui-monospace,monospace;font-size:11.5px;color:var(--muted);font-weight:700">#T-${String(i+1).padStart(3,'0')}</span><span style="font-weight:800;font-size:14.5px">${sicher(r.betreff)}</span></div>
|
||
<div style="display:flex;align-items:center;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)">Gemeldet von <b style="color:var(--fg)">${sicher(r.spieler)}</b></span></div></div>
|
||
<div style="display:flex;gap:7px;flex-shrink:0">${r.status==='geschlossen'?`<button class="btn ghost sm" data-reopen="${r.id}">Wieder öffnen</button>`:`<button class="btn ghost sm" data-claim="${r.id}">Übernehmen</button><button class="btn primary sm" data-answer="${r.id}">Antworten</button><button class="btn green sm" data-close="${r.id}">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",()=>{const r=reports.find(x=>x.id===b.dataset.claim);r.status="in Bearbeitung";log("Report übernommen",r.betreff);toast("Übernommen");renderSektion();renderNav();}));
|
||
$("main").querySelectorAll("[data-close]").forEach(b=>b.addEventListener("click",()=>{const r=reports.find(x=>x.id===b.dataset.close);r.status="geschlossen";log("Report geschlossen",r.betreff);toast("Geschlossen","good");renderSektion();renderNav();}));
|
||
$("main").querySelectorAll("[data-reopen]").forEach(b=>b.addEventListener("click",()=>{const r=reports.find(x=>x.id===b.dataset.reopen);r.status="offen";log("Report wieder geöffnet",r.betreff);toast("Wieder geöffnet");renderSektion();renderNav();}));
|
||
$("main").querySelectorAll("[data-answer]").forEach(b=>b.addEventListener("click",()=>{const r=reports.find(x=>x.id===b.dataset.answer);
|
||
modal(`<h3>Antwort · ${sicher(r.spieler)}</h3><div class="msub">${sicher(r.betreff)}</div><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",()=>{if(!$("ra").value.trim()){toast("Text eingeben","warn");return;}r.status="in Bearbeitung";log("Report beantwortet",r.betreff);closeModal();toast("Antwort gesendet","good");renderSektion();renderNav();});}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
RESSOURCEN
|
||
===================================================================== */
|
||
sektionen.ressourcen=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Ressourcen</h2><p>${ressourcen.filter(r=>r.status==='running').length}/${ressourcen.length} aktiv · TPS ${server.tps.toFixed(1)}</p></div>
|
||
<div style="display:flex;gap:8px"><button class="btn ghost" id="rz-save">💾 Speichern</button><button class="btn red" id="rz-restart">⟳ Server-Neustart</button></div></div>
|
||
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Ressource</th><th>Status</th><th>CPU</th><th>RAM</th><th></th></tr></thead><tbody>
|
||
${ressourcen.map((r,i)=>`<tr><td>🧩 ${sicher(r.name)}</td><td>${r.status==='running'?'<span class="pill g">läuft</span>':'<span class="pill r">gestoppt</span>'}</td>
|
||
<td class="num">${r.cpu.toFixed(1)}%</td><td class="num">${r.mem} MB</td>
|
||
<td class="num">${r.status==='running'?`<button class="btn ghost sm" data-restart="${i}">Restart</button> <button class="btn red sm" data-stop="${i}">Stop</button>`:`<button class="btn green sm" data-start="${i}">Start</button>`}</td></tr>`).join('')}
|
||
</tbody></table></div></div>`;
|
||
$("rz-save").addEventListener("click",()=>{log("Alles gespeichert");toast("Gespeichert","good");});
|
||
$("rz-restart").addEventListener("click",restartModal);
|
||
$("main").querySelectorAll("[data-restart]").forEach(b=>b.addEventListener("click",()=>{log("Ressource neugestartet",ressourcen[+b.dataset.restart].name);toast(ressourcen[+b.dataset.restart].name+" neugestartet","good");}));
|
||
$("main").querySelectorAll("[data-stop]").forEach(b=>b.addEventListener("click",()=>{const r=ressourcen[+b.dataset.stop];r.status="stopped";r.cpu=0;r.mem=0;log("Ressource gestoppt",r.name);toast(r.name+" gestoppt","warn");renderSektion();}));
|
||
$("main").querySelectorAll("[data-start]").forEach(b=>b.addEventListener("click",()=>{const r=ressourcen[+b.dataset.start];r.status="running";r.cpu=1+Math.random()*3;r.mem=20+Math.floor(Math.random()*40);log("Ressource gestartet",r.name);toast(r.name+" gestartet","good");renderSektion();}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
AUDIT-LOG
|
||
===================================================================== */
|
||
let logSuche="", logWer="alle";
|
||
sektionen.logs=()=>{
|
||
const admins=["alle",...new Set(logs.map(l=>l.wer))];
|
||
if(!admins.includes(logWer))logWer="alle";
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Audit-Log</h2><p>${logs.length} protokollierte Aktionen</p></div>
|
||
<button class="btn ghost" id="lg-export">⬇️ CSV exportieren</button></div>
|
||
<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 oder Detail suchen…" value="${sicher(logSuche)}"></div>
|
||
<select class="input" id="lg-wer" style="width:auto;flex:none">${admins.map(a=>`<option value="${sicher(a)}" ${a===logWer?'selected':''}>${a==='alle'?'Alle Admins':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>Detail</th></tr></thead><tbody id="lg-body"></tbody></table></div></div>`;
|
||
const s=$("lg-such");s.addEventListener("input",()=>{logSuche=s.value.trim().toLowerCase();zeichneLogs();});
|
||
$("lg-wer").addEventListener("change",e=>{logWer=e.target.value;zeichneLogs();});
|
||
$("lg-export").addEventListener("click",exportLogs);
|
||
zeichneLogs();
|
||
};
|
||
function logListe(){return logs.filter(l=>{
|
||
const q=!logSuche||(l.aktion+" "+l.detail).toLowerCase().includes(logSuche);
|
||
return q&&(logWer==="alle"||l.wer===logWer);});}
|
||
function zeichneLogs(){
|
||
const el=$("lg-body");if(!el)return;
|
||
el.innerHTML=logListe().map(l=>`<tr><td class="mono" style="color:var(--muted)">${l.zeit.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'})}</td><td>${sicher(l.wer)}</td><td>${sicher(l.aktion)}</td><td style="color:var(--muted)">${sicher(l.detail)}</td></tr>`).join('')||'<tr><td colspan="4" class="empty">Keine Treffer.</td></tr>';
|
||
}
|
||
function exportLogs(){
|
||
const rows=[["Zeit","Admin","Aktion","Detail"],...logListe().map(l=>[l.zeit.toLocaleString('de-DE'),l.wer,l.aktion,l.detail])];
|
||
const csv=rows.map(r=>r.map(c=>`"${String(c).replace(/"/g,'""')}"`).join(";")).join("\n");
|
||
const a=document.createElement("a");a.href="data:text/csv;charset=utf-8,"+encodeURIComponent(csv);a.download="audit-log.csv";a.click();
|
||
log("Audit-Log exportiert",logListe().length+" Zeilen");toast("CSV-Export erstellt","good");
|
||
}
|
||
|
||
/* =====================================================================
|
||
ADMIN-TEAM
|
||
===================================================================== */
|
||
const adminRollen=["Owner","Admin","Moderator","Supporter"];
|
||
sektionen.team=()=>{
|
||
const rolleP={Owner:'r',Admin:'a',Moderator:'b',Supporter:'v'};
|
||
const darf=admin.rolle==="Owner";
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Admin-Team</h2><p>${adminTeam.filter(a=>a.online).length}/${adminTeam.length} online${darf?'':' · nur der Owner darf das Team verwalten'}</p></div>${darf?'<button class="btn primary" id="tm-neu">+ Admin hinzufügen</button>':''}</div>
|
||
<div class="grid" style="grid-template-columns:repeat(2,1fr)">${adminTeam.map((a,i)=>`<div class="card"><div style="display:flex;align-items:center;gap:11px"><span class="av" style="width:40px;height:40px;background:${avColor(a.name)}">${initialen(a.name)}</span>
|
||
<div style="flex:1"><div style="font-weight:800">${sicher(a.name)}${a.name===admin.name?' <span class="pill m">du</span>':''}</div><div style="font-size:12px"><span class="pill ${rolleP[a.rolle]}">${a.rolle}</span></div></div>
|
||
<span class="pill ${a.online?'g':'m'}">${a.online?'online':'offline'}</span></div>
|
||
${darf?`<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap"><button class="btn ghost sm" data-trolle="${i}">Rolle ändern</button>${a.name===admin.name?'':`<button class="btn red sm" data-tdel="${i}">Entfernen</button>`}</div>`:''}</div>`).join('')}</div>
|
||
<div class="card" style="margin-top:12px;font-size:12.5px;color:var(--muted)">Rechte-Stufen: <b style="color:var(--red)">Owner</b> (alles) › <b style="color:var(--amber)">Admin</b> (Spieler/Welt/Fahrzeuge) › <b style="color:var(--accent)">Moderator</b> (Kick/Warn/Reports) › <b style="color:var(--violet)">Supporter</b> (Reports/Tickets).</div>`;
|
||
if(!darf)return;
|
||
$("tm-neu").addEventListener("click",teamAddModal);
|
||
$("main").querySelectorAll("[data-trolle]").forEach(b=>b.addEventListener("click",()=>teamRolleModal(+b.dataset.trolle)));
|
||
$("main").querySelectorAll("[data-tdel]").forEach(b=>b.addEventListener("click",()=>{const a=adminTeam[+b.dataset.tdel];
|
||
modal(`<h3>Admin entfernen?</h3><p style="color:var(--muted);font-size:13px">„${sicher(a.name)}“ (${a.rolle}) verliert alle Admin-Rechte.</p><div class="modal-actions"><button class="btn ghost" id="td-c">Abbrechen</button><button class="btn red" id="td-ok">Entfernen</button></div>`,true);
|
||
$("td-c").addEventListener("click",closeModal);$("td-ok").addEventListener("click",()=>{adminTeam.splice(adminTeam.indexOf(a),1);log("Admin entfernt",a.name+" ("+a.rolle+")");closeModal();toast(a.name+" entfernt","warn");renderSektion();});}));
|
||
};
|
||
function teamAddModal(){
|
||
modal(`<h3>Admin hinzufügen</h3><div class="msub">Vergibt Admin-Rechte an einen Spieler</div>
|
||
<label class="flabel">Name / Identifier</label><input class="input" id="ta-name" placeholder="z. B. Steam-Name oder Lizenz">
|
||
<label class="flabel">Rolle</label><select class="input" id="ta-rolle">${adminRollen.map(r=>`<option ${r==='Moderator'?'selected':''}>${r}</option>`).join('')}</select>
|
||
<div class="modal-actions"><button class="btn ghost" id="ta-c">Abbrechen</button><button class="btn primary" id="ta-ok">Hinzufügen</button></div>`,true);
|
||
$("ta-c").addEventListener("click",closeModal);
|
||
$("ta-ok").addEventListener("click",()=>{const n=$("ta-name").value.trim();if(!n){toast("Name angeben","warn");return;}
|
||
if(adminTeam.some(a=>a.name.toLowerCase()===n.toLowerCase())){toast("Ist bereits im Team","warn");return;}
|
||
const r=$("ta-rolle").value;adminTeam.push({name:n,rolle:r,online:false});log("Admin hinzugefügt",n+" ("+r+")");closeModal();toast(n+" als "+r+" hinzugefügt","good");renderSektion();});
|
||
}
|
||
function teamRolleModal(i){const a=adminTeam[i];const eigen=a.name===admin.name;
|
||
modal(`<h3>Rolle ändern · ${sicher(a.name)}</h3><div class="msub">Aktuell: ${a.rolle}</div>
|
||
<label class="flabel">Neue Rolle</label><select class="input" id="tr-rolle">${adminRollen.map(r=>`<option ${a.rolle===r?'selected':''}>${r}</option>`).join('')}</select>
|
||
${eigen?'<div class="card" style="margin-top:10px;font-size:12px;color:var(--amber)">⚠️ Du änderst deine eigene Rolle. Eine Herabstufung entzieht dir sofort Rechte.</div>':''}
|
||
<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",()=>{const r=$("tr-rolle").value;a.rolle=r;if(eigen){admin.rolle=r;renderTop();}log("Rolle geändert",a.name+" → "+r);closeModal();toast(a.name+" ist jetzt "+r,"good");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
EINSTELLUNGEN
|
||
===================================================================== */
|
||
const settings={maxSlots:64,pvp:true,whitelist:true,voice:true,economy:true};
|
||
sektionen.settings=()=>{
|
||
const sw=(k,label)=>`<div class="row" style="margin:0 0 8px"><div class="grow"><div class="t">${label}</div></div><button class="btn ${settings[k]?'green':'ghost'} sm" data-set="${k}">${settings[k]?'AN':'AUS'}</button></div>`;
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Einstellungen</h2><p>Server-Konfiguration</p></div></div>
|
||
<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(server.name)}">
|
||
<label class="flabel">Maximale Slots</label><input class="input" id="st-slots" type="number" value="${settings.maxSlots}">
|
||
<label class="flabel">Neustart-Intervall (Stunden)</label><input class="input" id="st-restart" type="number" min="0" step="1" value="${Math.round(server.neustart/60)}">
|
||
<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 aktiviert')}${sw('whitelist','Whitelist')}${sw('voice','Voice-Chat')}${sw('economy','Wirtschaftssystem')}</div></div>`;
|
||
$("st-save").addEventListener("click",()=>{
|
||
const name=$("st-name").value.trim()||server.name;server.name=name;
|
||
settings.maxSlots=parseInt($("st-slots").value,10)||64;server.slots=settings.maxSlots;
|
||
const std=Math.max(0,parseInt($("st-restart").value,10)||0);server.neustart=std*60;
|
||
log("Einstellungen gespeichert",name+" · "+settings.maxSlots+" Slots · Neustart alle "+std+"h");toast("Einstellungen gespeichert","good");renderTop();});
|
||
$("main").querySelectorAll("[data-set]").forEach(b=>b.addEventListener("click",()=>{settings[b.dataset.set]=!settings[b.dataset.set];log("Einstellung",b.dataset.set+" "+(settings[b.dataset.set]?"an":"aus"));renderSektion();}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
WELTKARTE (Live-Standorte)
|
||
===================================================================== */
|
||
const frakFarbe=f=>({LSPD:"#4aa8ff",EMS:"#f0506e",Ballas:"#7c5cf6",Vagos:"#f5b23d"}[f]||"#34d39a");
|
||
/* Quick-Menü für Spieler (Karte & Listen) */
|
||
function quickMenu(p,ev){
|
||
if(!p)return; const box=$("qmenu");
|
||
const acts=[
|
||
["⚙️ Verwalten (alle Aktionen)",()=>spielerModal(p.id)],
|
||
["📹 Live-Kamera",()=>einzelKamera(p)],
|
||
["🧭 Zu Spieler",()=>{log("goto",p.name);toast("Zu "+p.name+" teleportiert","good");}],
|
||
["📥 Spieler herholen",()=>{log("bring",p.name);toast(p.name+" hergeholt","good");}],
|
||
["❤️ Heilen",()=>{log("heal",p.name);toast(p.name+" geheilt","good");}],
|
||
[p.frozen?"🔥 Auftauen":"🧊 Einfrieren",()=>{p.frozen=!p.frozen;log("freeze",p.name);toast(p.name+(p.frozen?" eingefroren":" aufgetaut"));if(sektion==="weltkarte"||sektion==="spieler")renderSektion();}],
|
||
["👢 Kicken",()=>kickModal(p),"danger"]
|
||
];
|
||
box.innerHTML=`<div class="qm-h">${sicher(p.name)} · ${sicher(p.region)}</div>`+acts.map((a,i)=>`<button data-qa="${i}" class="${a[2]||''}">${a[0]}</button>`).join("");
|
||
box.classList.add("show");
|
||
let x=Math.min(ev.clientX,window.innerWidth-box.offsetWidth-8), y=Math.min(ev.clientY,window.innerHeight-box.offsetHeight-8);
|
||
box.style.left=x+"px"; box.style.top=y+"px";
|
||
box.querySelectorAll("[data-qa]").forEach(b=>b.addEventListener("click",()=>{schliesseQuick();acts[+b.dataset.qa][1]();}));
|
||
}
|
||
function schliesseQuick(){$("qmenu").classList.remove("show");}
|
||
document.addEventListener("click",e=>{if(!e.target.closest("#qmenu"))schliesseQuick();});
|
||
|
||
sektionen.weltkarte=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Weltkarte</h2><p>${players.length} Spieler live · Marker oder Zeile anklicken für das Quick-Menü</p></div></div>
|
||
<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 class="distr" style="left:64%;top:66%">Del Perro</div><div class="distr" style="left:40%;top:44%">Legion Sq.</div>
|
||
${players.map(p=>`<div class="mk${p.tot?' dead':''}" id="mk-${p.id}" data-mk="${p.id}" style="left:${p.posX}%;top:${p.posY}%"><div class="dot" style="background:${frakFarbe(p.fraktion)}">${initialen(p.name)}</div><div class="lbl">${sicher(p.name.split(' ')[0])}</div></div>`).join('')}</div>
|
||
<div style="overflow-y:auto"><div class="list-h">Spieler online <span class="r">${players.length}</span></div>
|
||
${players.map(p=>`<div class="row" style="padding:8px 10px;margin-bottom:6px;cursor:pointer" data-pq="${p.id}"><span class="av" style="width:28px;height:28px;font-size:10px;background:${frakFarbe(p.fraktion)}">${initialen(p.name)}</span><div class="grow"><div class="t" style="font-size:13px">${sicher(p.name)}</div><div class="m">${sicher(p.region)}${p.fraktion?' · '+p.fraktion:''} · ${p.ping} ms</div></div><span style="color:var(--muted);font-size:16px">›</span></div>`).join('')}</div></div>`;
|
||
$("main").querySelectorAll("[data-mk]").forEach(m=>m.addEventListener("click",e=>{e.stopPropagation();quickMenu(player(+m.dataset.mk),e);}));
|
||
$("main").querySelectorAll("[data-pq]").forEach(r=>r.addEventListener("click",e=>{e.stopPropagation();quickMenu(player(+r.dataset.pq),e);}));
|
||
clearInterval(mapTimer);
|
||
mapTimer=setInterval(()=>{players.forEach(p=>{p.posX=Math.max(5,Math.min(95,p.posX+(Math.random()-0.5)*5));p.posY=Math.max(7,Math.min(92,p.posY+(Math.random()-0.5)*5));const el=$("mk-"+p.id);if(el){el.style.left=p.posX+'%';el.style.top=p.posY+'%';}});},2800);
|
||
};
|
||
|
||
/* =====================================================================
|
||
LIVE-KAMERAS (WebRTC-Spectate / Multi-Cam-Views)
|
||
===================================================================== */
|
||
function szeneFarbe(region){let h=0;for(const c of (region||""))h=(h*31+c.charCodeAt(0))%360;return `linear-gradient(180deg,hsl(${h} 32% 22%),hsl(${(h+40)%360} 26% 9%))`;}
|
||
function camFeed(pid,voll){const p=player(pid);if(!p)return '';
|
||
return `<div class="camtile" data-tile="${pid}"><div class="camfeed"><div class="scene" style="background:${szeneFarbe(p.region)}"></div><div class="sweep"></div><div class="noise"></div><div class="scan"></div></div>
|
||
<div class="cam-hud"><div class="who">CAM · ${sicher(p.name)} #${p.id}</div><div class="rec"><i></i>LIVE</div><div class="cross"></div>
|
||
<div class="foot"><span>📍 ${sicher(p.region)}</span><span class="cam-time">${new Date().toLocaleTimeString('de-DE')}</span><span>${p.ping} ms</span></div></div>
|
||
${voll?'':`<div class="ctrls"><button data-full="${pid}">⛶</button><button data-goto2="${pid}">Goto</button><button data-rmcam="${pid}">✕</button></div>`}</div>`;
|
||
}
|
||
function einzelKamera(p){
|
||
let v=camViews.find(x=>x.id==="single"); if(!v){v={id:"single",name:"Einzelkamera",spieler:[]};camViews.unshift(v);}
|
||
v.spieler=[p.id]; v.name="Kamera: "+p.name; aktiveView="single"; sektion="cams"; closeModal(); log("Live-Kamera geöffnet",p.name); renderNav(); renderSektion();
|
||
}
|
||
sektionen.cams=()=>{
|
||
if(!camViews.find(v=>v.id===aktiveView))aktiveView=camViews[0]&&camViews[0].id;
|
||
const v=camViews.find(x=>x.id===aktiveView);
|
||
const cols=!v?1:v.spieler.length<=1?1:v.spieler.length<=4?2:3;
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Live-Kameras</h2><p>WebRTC-Spectate · Einzel- & Multi-Cam-Views</p></div><button class="btn primary" id="cam-new">+ View erstellen</button></div>
|
||
<div style="display:grid;grid-template-columns:230px 1fr;gap:12px;align-items:start">
|
||
<div class="viewlist">${camViews.map(x=>`<button class="btn ${x.id===aktiveView?'primary':'ghost'}" data-v="${x.id}">${sicher(x.name)} · ${x.spieler.length}📹</button>`).join('')||'<div class="empty">Keine Views.</div>'}
|
||
${v?`<button class="btn ghost sm" id="view-edit" style="width:100%;margin-top:6px">Spieler bearbeiten</button><button class="btn ghost sm" id="view-del" style="width:100%;margin-top:6px">View löschen</button>`:''}</div>
|
||
<div>${v&&v.spieler.length?`<div class="camgrid" style="grid-template-columns:repeat(${cols},1fr)">${v.spieler.map(pid=>camFeed(pid)).join('')}</div>`:'<div class="empty" style="padding:60px"><div class="big">📹</div>Keine Spieler in dieser View — füge welche hinzu.</div>'}</div></div>`;
|
||
$("cam-new").addEventListener("click",()=>viewModal(null));
|
||
$("main").querySelectorAll("[data-v]").forEach(b=>b.addEventListener("click",()=>{aktiveView=b.dataset.v;renderSektion();}));
|
||
const ve=$("view-edit");if(ve)ve.addEventListener("click",()=>viewModal(v));
|
||
const vd=$("view-del");if(vd)vd.addEventListener("click",()=>{camViews=camViews.filter(x=>x.id!==v.id);aktiveView=camViews[0]&&camViews[0].id;log("Cam-View gelöscht",v.name);toast("View gelöscht");renderSektion();});
|
||
bindCamCtrls();
|
||
};
|
||
function bindCamCtrls(){
|
||
$("main").querySelectorAll("[data-rmcam]").forEach(b=>b.addEventListener("click",()=>{const v=camViews.find(x=>x.id===aktiveView);v.spieler=v.spieler.filter(id=>id!==+b.dataset.rmcam);renderSektion();}));
|
||
$("main").querySelectorAll("[data-goto2]").forEach(b=>b.addEventListener("click",()=>{const p=player(+b.dataset.goto2);log("goto (Kamera)",p.name);toast("Zu "+p.name+" teleportiert","good");}));
|
||
$("main").querySelectorAll("[data-full]").forEach(b=>b.addEventListener("click",()=>{const p=player(+b.dataset.full);modal(`<h3>📹 ${sicher(p.name)} · Live</h3><div class="msub">📍 ${sicher(p.region)} · ${p.ping} ms</div>${camFeed(p.id,true)}<div class="modal-actions" style="grid-template-columns:1fr"><button class="btn ghost" id="fc-close">Schließen</button></div>`);$("fc-close").addEventListener("click",closeModal);}));
|
||
}
|
||
function viewModal(v){const editing=!!v;const sel=new Set(editing?v.spieler:[]);
|
||
modal(`<h3>${editing?'View bearbeiten':'Neue Live-View'}</h3><div class="msub">Mehrere Spieler wählen → Multi-Cam (z. B. Gang-Fight-Kontrolle)</div>
|
||
${editing?'':'<label class="flabel">Name der View</label><input class="input" id="vw-name" placeholder="z. B. Gang Fight Grove St.">'}
|
||
<label class="flabel">Spieler</label><div style="max-height:280px;overflow-y:auto">${players.map(p=>`<div class="chk${sel.has(p.id)?' sel':''}" data-pv="${p.id}"><span class="av" style="background:${avColor(p.name)}">${initialen(p.name)}</span><div class="grow" style="font-weight:700;font-size:13px">${sicher(p.name)}<span style="color:var(--muted);font-weight:500"> · ${sicher(p.region)}</span></div></div>`).join('')}</div>
|
||
<div class="modal-actions"><button class="btn ghost" id="vw-cancel">Abbrechen</button><button class="btn primary" id="vw-ok">${editing?'Speichern':'Erstellen'}</button></div>`);
|
||
$("overlay").querySelectorAll("[data-pv]").forEach(el=>el.addEventListener("click",()=>{const id=+el.dataset.pv;if(sel.has(id)){sel.delete(id);el.classList.remove('sel');}else{sel.add(id);el.classList.add('sel');}}));
|
||
$("vw-cancel").addEventListener("click",()=>{closeModal();renderSektion();});
|
||
$("vw-ok").addEventListener("click",()=>{
|
||
if(editing){v.spieler=[...sel];}
|
||
else{const name=($("vw-name").value||"").trim()||"Neue View";const id="cv"+Date.now();camViews.push({id,name,spieler:[...sel]});aktiveView=id;log("Cam-View erstellt",name+" ("+sel.size+" Spieler)");}
|
||
closeModal();toast("View gespeichert","good");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
KILL-/DEATHCAMS
|
||
===================================================================== */
|
||
function alterText(t){const min=Math.floor((Date.now()-t)/60000);if(min<60)return "vor "+min+" min";const h=Math.floor(min/60);return "vor "+h+" h";}
|
||
function killcamCard(k){const alt=Date.now()-k.zeit,rest=Math.max(0,Math.ceil((86400000-alt)/3600000));
|
||
return `<div class="kc"><div class="thumb"><div style="position:absolute;inset:0;background:${szeneFarbe(k.ort)}"></div><div class="scan"></div><div class="pl">▶</div></div>
|
||
<div class="grow"><div style="font-weight:800;font-size:14px">${sicher(k.killer)} <span style="color:var(--red)">▸</span> ${sicher(k.opfer)}</div>
|
||
<div style="font-size:12px;color:var(--muted);margin-top:2px">🔫 ${sicher(k.waffe)} · ${k.distanz} m · 📍 ${sicher(k.ort)} · ${alterText(k.zeit)}</div>
|
||
<div style="margin-top:6px">${k.gespeichert?'<span class="pill g">💾 gespeichert – bleibt erhalten</span>':(rest>0?`<span class="pill a">wird in ${rest} h automatisch gelöscht</span>`:'<span class="pill r">überfällig – wird gelöscht</span>')}</div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px"><button class="btn primary sm" data-view="${k.id}">Ansehen</button><button class="btn ${k.gespeichert?'ghost':'green'} sm" data-save="${k.id}">${k.gespeichert?'Freigeben':'💾 Speichern'}</button><button class="btn ghost sm" data-del="${k.id}">Löschen</button></div></div>`;
|
||
}
|
||
sektionen.killcams=()=>{
|
||
const list=killcams.filter(k=>kcFilter==='alle'||k.gespeichert);
|
||
const alt=killcams.filter(k=>!k.gespeichert&&(Date.now()-k.zeit)>86400000).length;
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Kill- / Deathcams</h2><p>${killcams.length} Aufnahmen · nicht gespeicherte werden nach 24 h gelöscht</p></div>
|
||
<div style="display:flex;gap:8px"><div class="seg" id="kc-filter"><button data-f="alle" class="${kcFilter==='alle'?'active':''}">Alle</button><button data-f="saved" class="${kcFilter==='saved'?'active':''}">Gespeichert</button></div>${alt?`<button class="btn red" id="kc-clean">${alt} alte löschen</button>`:''}</div></div>
|
||
${list.map(killcamCard).join('')||'<div class="empty"><div class="big">🎬</div>Keine Aufnahmen.</div>'}`;
|
||
$("kc-filter").querySelectorAll("[data-f]").forEach(b=>b.addEventListener("click",()=>{kcFilter=b.dataset.f;renderSektion();}));
|
||
const cl=$("kc-clean");if(cl)cl.addEventListener("click",()=>{const n=killcams.length;killcams=killcams.filter(k=>k.gespeichert||(Date.now()-k.zeit)<=86400000);log("Alte Aufnahmen gelöscht",(n-killcams.length)+" entfernt");toast((n-killcams.length)+" alte Aufnahmen gelöscht","warn");renderSektion();});
|
||
$("main").querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>killcamViewer(b.dataset.view)));
|
||
$("main").querySelectorAll("[data-save]").forEach(b=>b.addEventListener("click",()=>{const k=killcams.find(x=>x.id===b.dataset.save);k.gespeichert=!k.gespeichert;log("Aufnahme "+(k.gespeichert?"gespeichert":"freigegeben"),k.killer+" ▸ "+k.opfer);toast(k.gespeichert?"Gespeichert – bleibt erhalten":"Speicherung entfernt");renderSektion();}));
|
||
$("main").querySelectorAll("[data-del]").forEach(b=>b.addEventListener("click",()=>{killcams=killcams.filter(x=>x.id!==b.dataset.del);toast("Aufnahme gelöscht");renderSektion();}));
|
||
};
|
||
function killcamViewer(id){const k=killcams.find(x=>x.id===id);
|
||
modal(`<h3>🎬 Replay · ${sicher(k.killer)} ▸ ${sicher(k.opfer)}</h3><div class="msub">${sicher(k.waffe)} · ${k.distanz} m · ${sicher(k.ort)} · ${alterText(k.zeit)}</div>
|
||
<div class="camtile" style="aspect-ratio:16/9"><div class="camfeed"><div class="scene" style="background:${szeneFarbe(k.ort)}"></div><div class="sweep"></div><div class="noise"></div><div class="scan"></div></div>
|
||
<div class="cam-hud"><div class="who">REPLAY · ${sicher(k.opfer)} †</div><div class="rec" style="color:#fff"><i style="background:#fff"></i>REC</div><div class="cross"></div><div class="foot"><span>${sicher(k.killer)} ▸ ${sicher(k.opfer)}</span><span>${sicher(k.waffe)} · ${k.distanz} m</span></div></div></div>
|
||
<input type="range" min="0" max="100" value="60" style="width:100%;accent-color:var(--accent);margin-top:10px">
|
||
<div class="modal-actions"><button class="btn ${k.gespeichert?'ghost':'green'}" id="kv-save">${k.gespeichert?'Speicherung entfernen':'💾 Speichern'}</button><button class="btn ghost" id="kv-close">Schließen</button></div>`);
|
||
$("kv-close").addEventListener("click",closeModal);
|
||
$("kv-save").addEventListener("click",()=>{k.gespeichert=!k.gespeichert;log("Aufnahme "+(k.gespeichert?"gespeichert":"freigegeben"));closeModal();toast(k.gespeichert?"Gespeichert":"Freigegeben");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
STAATSPROJEKTE (aktivierbar nach positiver Abstimmung)
|
||
===================================================================== */
|
||
function projektCard(p){const gesamt=p.ja+p.nein,proz=gesamt?Math.round(p.ja/gesamt*100):0,positiv=proz>=ZUSTIMMUNG_MIN*100;
|
||
let status,aktion="";
|
||
if(p.aktiviert){status='<span class="pill g">✔ Umgesetzt</span>';}
|
||
else if(!p.abgelaufen){status='<span class="pill b">Abstimmung läuft</span>';aktion=`<button class="btn ghost sm" data-end="${p.id}">Abstimmung beenden</button>`;}
|
||
else if(positiv){status='<span class="pill a">bereit zur Umsetzung</span>';aktion=`<button class="btn green sm" data-act="${p.id}">Aktivieren</button>`;}
|
||
else {status='<span class="pill r">abgelehnt · zu wenig Zustimmung</span>';aktion=`<button class="btn ghost sm" data-reject="${p.id}">Archivieren</button>`;}
|
||
return `<div class="card" style="margin-bottom:10px"><div style="display:flex;align-items:flex-start;gap:12px"><div class="grow" style="flex:1">
|
||
<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:${proz}%;background:var(--green)"></div><div style="width:${100-proz}%;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)'}">${proz}% Zustimmung</b> (nötig: ${ZUSTIMMUNG_MIN*100}%)</div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px">${aktion}</div></div></div>`;
|
||
}
|
||
sektionen.staat=()=>{
|
||
const offen=staatsprojekte.filter(p=>!p.aktiviert).length, umgesetzt=staatsprojekte.filter(p=>p.aktiviert).length;
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Staatsprojekte</h2><p>Abstimmung aus der Handy-Staat-App · ${offen} offen · ${umgesetzt} umgesetzt</p></div></div>
|
||
<div class="card" style="margin-bottom:12px;font-size:12px;color:var(--muted)">Ein Projekt kann <b style="color:var(--fg)">aktiviert</b> werden, sobald die <b style="color:var(--fg)">Abstimmung beendet</b> ist und mindestens <b style="color:var(--green)">${ZUSTIMMUNG_MIN*100}% Zustimmung</b> erreicht wurden. Die Kosten werden aus der Staatskasse (${euro(economy.staatskasse)}) beglichen.</div>
|
||
${staatsprojekte.map(projektCard).join('')}`;
|
||
$("main").querySelectorAll("[data-end]").forEach(b=>b.addEventListener("click",()=>{const p=staatsprojekte.find(x=>x.id===b.dataset.end);p.abgelaufen=true;log("Abstimmung beendet",p.titel);toast("Abstimmung beendet");renderSektion();renderNav();}));
|
||
$("main").querySelectorAll("[data-act]").forEach(b=>b.addEventListener("click",()=>{const p=staatsprojekte.find(x=>x.id===b.dataset.act);if(p.kosten>economy.staatskasse){toast("Staatskasse nicht gedeckt","warn");return;}economy.staatskasse-=p.kosten;p.aktiviert=true;log("Staatsprojekt aktiviert",p.titel+" ("+euro(p.kosten)+")");toast("✔ „"+p.titel+"“ umgesetzt","good");renderSektion();renderNav();}));
|
||
$("main").querySelectorAll("[data-reject]").forEach(b=>b.addEventListener("click",()=>{const p=staatsprojekte.find(x=>x.id===b.dataset.reject);staatsprojekte=staatsprojekte.filter(x=>x.id!==b.dataset.reject);log("Staatsprojekt archiviert",p.titel);toast("Archiviert");renderSektion();renderNav();}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
LIVE-TICK & START
|
||
===================================================================== */
|
||
setInterval(()=>{
|
||
server.tps=Math.max(15,Math.min(20,server.tps+(Math.random()-0.5)*0.6));
|
||
server.cpu=Math.max(25,Math.min(70,server.cpu+Math.floor((Math.random()-0.5)*4)));
|
||
if(server.neustart>0&&Math.random()<0.5)server.neustart--;
|
||
server.uptimeMin++;
|
||
players.forEach(p=>{p.ping=Math.max(15,Math.min(200,p.ping+Math.floor((Math.random()-0.5)*8)));});
|
||
document.querySelectorAll(".cam-time").forEach(e=>e.textContent=new Date().toLocaleTimeString("de-DE"));
|
||
renderTop();
|
||
if(sektion==="dashboard"){const k=$("kpi-tps");if(k)k.textContent=server.tps.toFixed(1);const c=$("kpi-cpu");if(c)c.textContent=server.cpu;}
|
||
},3000);
|
||
|
||
$("overlay").addEventListener("click",e=>{if(e.target===$("overlay"))closeModal();});
|
||
refresh();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|