Files
fuesse-sexy/public/admin.html
Guido 5018ab753a 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>
2026-08-05 22:19:52 +02:00

138 lines
6.4 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.
<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>