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

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

978 lines
111 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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- &amp; 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>