Admin-Panel (/admin): Auth, Spieler-Tracking, Live-Tuning, Bild-Verwaltung
- Passwortgeschütztes Panel unter /admin (Auto-Passwort 20 Zeichen, per CLI abrufbar: `node server/admin.js password` / `password reset`), In-Memory-Sessions + Cookie, Login-Rate-Limit. - Spieler-Tracking: pro Score IP (CF/XFF/req.ip), Gerät/Browser (ua-parser-js), grober Ort (geoip-lite, offline) -> data/players.json; Aggregation pro Name. - Live-Tuning: configStore (data/config.json) für Timer + Schwierigkeitskurve; Server (makeRound) und Client (/api/config) lesen daraus -> Änderungen ohne Neustart. - Bild-Verwaltung: Upload/Löschen von Füßen & Deko (multer) + rescanLibrary(). - Panel-Aktionen: Spieler bannen/entbannen, Rangliste heute/alles leeren. - Fix: .item object-fit:contain -> nicht-quadratische Bilder werden nicht mehr verzerrt. - trust proxy für korrekte Client-IP hinter Reverse-Proxy/Cloudflare. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
137
public/admin.html
Normal file
137
public/admin.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>füsse.sexy — Admin</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='88'>🛠️</text></svg>" />
|
||||
<style>
|
||||
:root { --bg:#0f1020; --panel:#1a1b33; --panel2:#232544; --pink:#ff4d8d; --text:#e9e9f5; --muted:#a0a0c0; --ok:#2ecc71; --warn:#ff5a5f; --border:#33345a; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif; }
|
||||
a { color:var(--pink); }
|
||||
h1 { color:var(--pink); margin:0; font-size:22px; }
|
||||
.wrap { max-width:1000px; margin:0 auto; padding:18px; }
|
||||
input, button, select { font:inherit; }
|
||||
input[type=password], input[type=text], input[type=number] {
|
||||
background:var(--panel2); border:1px solid var(--border); color:var(--text);
|
||||
border-radius:8px; padding:8px 10px; width:100%;
|
||||
}
|
||||
button { cursor:pointer; border:none; border-radius:8px; padding:9px 14px; font-weight:600; background:var(--panel2); color:var(--text); border:1px solid var(--border); }
|
||||
button.primary { background:var(--pink); color:#fff; border:none; }
|
||||
button.danger { background:#3a1420; color:#ff9bb2; border:1px solid #5a2233; }
|
||||
button:hover { filter:brightness(1.12); }
|
||||
.muted { color:var(--muted); font-size:13px; }
|
||||
.card { background:var(--panel); border:1px solid var(--border); border-radius:14px; padding:16px; margin-bottom:16px; }
|
||||
|
||||
/* Login */
|
||||
#login { max-width:380px; margin:12vh auto; }
|
||||
#login .row { display:flex; gap:8px; margin-top:10px; }
|
||||
#login-err { color:var(--warn); min-height:18px; margin-top:8px; font-size:14px; }
|
||||
|
||||
/* Panel */
|
||||
header.top { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; }
|
||||
.tabs { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:14px; }
|
||||
.tabs button.active { background:var(--pink); color:#fff; border:none; }
|
||||
section.tab { display:none; }
|
||||
section.tab.active { display:block; }
|
||||
|
||||
table { width:100%; border-collapse:collapse; font-size:14px; }
|
||||
th, td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--border); vertical-align:top; }
|
||||
th { color:var(--muted); font-weight:600; }
|
||||
.chips { display:flex; flex-wrap:wrap; gap:4px; }
|
||||
.chip { background:var(--panel2); border:1px solid var(--border); border-radius:999px; padding:2px 8px; font-size:12px; }
|
||||
|
||||
/* Settings */
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:12px; }
|
||||
.field label { display:block; font-size:13px; color:var(--muted); margin-bottom:4px; }
|
||||
|
||||
/* Images */
|
||||
.gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(110px,1fr)); gap:10px; }
|
||||
.thumb { background:var(--panel2); border:1px solid var(--border); border-radius:10px; padding:8px; text-align:center; }
|
||||
.thumb img { width:100%; height:72px; object-fit:contain; }
|
||||
.thumb .fn { font-size:11px; color:var(--muted); word-break:break-all; margin:6px 0; }
|
||||
.uprow { display:flex; gap:8px; align-items:center; margin-top:10px; flex-wrap:wrap; }
|
||||
.toast { position:fixed; left:50%; bottom:20px; transform:translateX(-50%); background:#000a; border:1px solid var(--border); color:#fff; padding:10px 16px; border-radius:10px; opacity:0; transition:opacity .2s; }
|
||||
.toast.show { opacity:1; }
|
||||
.hidden { display:none !important; }
|
||||
code { background:var(--panel2); padding:1px 5px; border-radius:5px; }
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
<!-- Login -->
|
||||
<div id="login" class="card hidden">
|
||||
<h1>🛠️ Admin-Login</h1>
|
||||
<p class="muted">Passwort per CLI: <code>node server/admin.js password</code></p>
|
||||
<div class="row">
|
||||
<input id="pw" type="password" placeholder="Passwort" autocomplete="current-password" />
|
||||
<button id="login-btn" class="primary">Login</button>
|
||||
</div>
|
||||
<div id="login-err"></div>
|
||||
</div>
|
||||
|
||||
<!-- Panel -->
|
||||
<div id="panel" class="hidden">
|
||||
<header class="top">
|
||||
<h1>🛠️ füsse.sexy — Admin</h1>
|
||||
<button id="logout-btn">Logout</button>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<button data-tab="players" class="active">Spieler</button>
|
||||
<button data-tab="settings">Einstellungen</button>
|
||||
<button data-tab="images">Bilder</button>
|
||||
<button data-tab="board">Rangliste</button>
|
||||
</div>
|
||||
|
||||
<section class="tab active" id="tab-players">
|
||||
<div class="card">
|
||||
<p class="muted">Approximative Zuordnung über IP, groben Ort, Gerät & Browser (kein Login vorhanden).</p>
|
||||
<div id="players-wrap">Lade …</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab" id="tab-settings">
|
||||
<div class="card">
|
||||
<p class="muted">Änderungen wirken sofort beim nächsten Spielstart (kein Neustart).</p>
|
||||
<div id="settings-grid" class="grid"></div>
|
||||
<div class="uprow">
|
||||
<button id="cfg-save" class="primary">Speichern</button>
|
||||
<span id="cfg-status" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab" id="tab-images">
|
||||
<div class="card">
|
||||
<h3>Füße <span class="muted">(die gesuchten Figuren)</span></h3>
|
||||
<div id="feet-gallery" class="gallery"></div>
|
||||
<div class="uprow">
|
||||
<input type="file" id="feet-file" accept=".svg,.png,.webp,.jpg,.jpeg,.gif" />
|
||||
<button class="primary" data-up="feet">Hochladen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Deko <span class="muted">(Verwirr-Bilder, nie gesucht)</span></h3>
|
||||
<div id="props-gallery" class="gallery"></div>
|
||||
<div class="uprow">
|
||||
<input type="file" id="props-file" accept=".svg,.png,.webp,.jpg,.jpeg,.gif" />
|
||||
<button class="primary" data-up="props">Hochladen</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">💡 Am besten <b>quadratische</b> Bilder: 256×256 oder 512×512 px PNG mit transparentem
|
||||
Hintergrund (oder SVG mit quadratischem viewBox). Nicht-quadratische werden dank <code>object-fit</code>
|
||||
nicht verzerrt, sondern nur eingepasst.</p>
|
||||
</section>
|
||||
|
||||
<section class="tab" id="tab-board">
|
||||
<div class="card">
|
||||
<h3>Rangliste zurücksetzen</h3>
|
||||
<div class="uprow">
|
||||
<button class="danger" data-reset="today">Heute leeren</button>
|
||||
<button class="danger" data-reset="all">Alles leeren</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
<script src="/js/admin.js"></script>
|
||||
Reference in New Issue
Block a user