// Admin-Panel-Logik. Auth laeuft ueber Cookie-Session; alle /api/admin/* sind geschuetzt. const $ = (id) => document.getElementById(id); async function api(path, opts = {}) { const res = await fetch(path, opts); let data = null; try { data = await res.json(); } catch {} return { ok: res.ok, status: res.status, data }; } function toast(msg) { const t = $('toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove('show'), 1800); } const esc = (s) => String(s == null ? '' : s); function fmt(iso) { try { return new Date(iso).toLocaleString('de-DE'); } catch { return iso; } } // ---------- Login ---------- function showLogin() { $('login').classList.remove('hidden'); $('panel').classList.add('hidden'); $('pw').focus(); } function showPanel() { $('login').classList.add('hidden'); $('panel').classList.remove('hidden'); loadPlayers(); } async function doLogin() { const password = $('pw').value; const { ok, status } = await api('/api/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), }); if (ok) { $('pw').value = ''; $('login-err').textContent = ''; showPanel(); } else $('login-err').textContent = status === 429 ? 'Zu viele Versuche — kurz warten.' : 'Falsches Passwort.'; } $('login-btn').addEventListener('click', doLogin); $('pw').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); }); $('logout-btn').addEventListener('click', async () => { await api('/api/admin/logout', { method: 'POST' }); showLogin(); }); // ---------- Tabs ---------- document.querySelectorAll('.tabs button').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.tabs button').forEach((b) => b.classList.remove('active')); document.querySelectorAll('section.tab').forEach((s) => s.classList.remove('active')); btn.classList.add('active'); const tab = btn.dataset.tab; $('tab-' + tab).classList.add('active'); if (tab === 'players') loadPlayers(); if (tab === 'settings') loadSettings(); if (tab === 'images') loadImages(); }); }); // ---------- Spieler ---------- async function loadPlayers() { const { ok, data } = await api('/api/admin/players'); if (!ok) return showLogin(); const bans = new Set((data.bans || []).map((b) => String(b).toLowerCase())); const players = data.players || []; if (!players.length) { $('players-wrap').innerHTML = '

Noch keine Spieler erfasst.

'; return; } const rows = players.map((p) => { const banned = bans.has(String(p.name).toLowerCase()); const chips = (arr) => (arr || []).map((x) => ``).join(''); const tr = document.createElement('tr'); tr.innerHTML = `${banned ? ' gebannt' : ''}` + `${p.bestScore}${p.games}${fmt(p.lastAt)}` + `` + ``; tr.querySelector('.pname').textContent = p.name; // XSS-safe const fill = (sel, arr) => { const c = tr.querySelector(sel); (arr || []).forEach((x) => { const s = document.createElement('span'); s.className = 'chip'; s.textContent = x; c.appendChild(s); }); }; fill('.ips', p.ips); fill('.locs', p.locations); fill('.devs', p.devices); fill('.brs', p.browsers); tr.querySelector('.act').addEventListener('click', () => (banned ? unban(p.name) : ban(p.name))); return tr; }); const table = document.createElement('table'); table.innerHTML = 'NameBestSpieleZuletztIP(s)OrtGerätBrowser'; const tbody = document.createElement('tbody'); rows.forEach((r) => tbody.appendChild(r)); table.appendChild(tbody); $('players-wrap').innerHTML = ''; $('players-wrap').appendChild(table); } async function ban(name) { if (!confirm(`"${name}" bannen? Bestehende Einträge werden entfernt.`)) return; await api('/api/admin/ban', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); toast('Gebannt: ' + name); loadPlayers(); } async function unban(name) { await api('/api/admin/unban', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); toast('Entbannt: ' + name); loadPlayers(); } // ---------- Einstellungen ---------- const CFG_LABELS = { startTime: 'Startzeit (s)', maxTime: 'Max-Zeit (s)', bonus: 'Bonus pro Treffer (s)', penalty: 'Strafe pro Fehlklick (s)', countBase: 'Objekte: Basis', countPerScore: 'Objekte: pro Score', stepStart: 'Stufen ab Score', stepEvery: 'Stufe alle N Punkte', stepAdd: 'Objekte pro Stufe', countCap: 'Objekte max.', stageScatter: 'Ab Score: verstreut', stageGroups: 'Ab Score: Gruppen', stageIndividual: 'Ab Score: einzeln', speedGroupsBase: 'Tempo Gruppen (Basis)', speedGroupsPer: 'Tempo Gruppen (pro Score)', speedGroupsMax: 'Tempo Gruppen (max)', speedIndivBase: 'Tempo einzeln (Basis)', speedIndivPer: 'Tempo einzeln (pro Score)', speedIndivMax: 'Tempo einzeln (max)', }; async function loadSettings() { const { ok, data } = await api('/api/admin/config'); if (!ok) return showLogin(); const grid = $('settings-grid'); grid.innerHTML = ''; Object.keys(data).forEach((key) => { const wrap = document.createElement('div'); wrap.className = 'field'; const label = document.createElement('label'); label.textContent = CFG_LABELS[key] || key; const input = document.createElement('input'); input.type = 'number'; input.step = 'any'; input.dataset.key = key; input.value = data[key]; wrap.append(label, input); grid.appendChild(wrap); }); } $('cfg-save').addEventListener('click', async () => { const body = {}; document.querySelectorAll('#settings-grid input').forEach((i) => (body[i.dataset.key] = Number(i.value))); const { ok } = await api('/api/admin/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); $('cfg-status').textContent = ok ? '✓ gespeichert' : 'Fehler'; if (ok) toast('Einstellungen gespeichert'); setTimeout(() => ($('cfg-status').textContent = ''), 2500); }); // ---------- Bilder ---------- async function loadImages() { const { ok, data } = await api('/api/admin/images'); if (!ok) return showLogin(); renderGallery('feet-gallery', 'feet', data.feet || []); renderGallery('props-gallery', 'props', data.props || []); } function renderGallery(elId, kind, list) { const el = $(elId); el.innerHTML = ''; if (!list.length) { el.innerHTML = '

Keine Bilder.

'; return; } list.forEach((img) => { const file = img.url.split('/').pop(); const div = document.createElement('div'); div.className = 'thumb'; const im = document.createElement('img'); im.src = img.url; im.alt = img.name; const fn = document.createElement('div'); fn.className = 'fn'; fn.textContent = file; const del = document.createElement('button'); del.className = 'danger'; del.textContent = 'Löschen'; del.style.width = '100%'; del.addEventListener('click', () => deleteImage(kind, file)); div.append(im, fn, del); el.appendChild(div); }); } async function deleteImage(kind, file) { if (!confirm(`"${file}" löschen?`)) return; const { ok } = await api(`/api/admin/images/${kind}/${encodeURIComponent(file)}`, { method: 'DELETE' }); if (ok) { toast('Gelöscht'); loadImages(); } else toast('Löschen fehlgeschlagen'); } document.querySelectorAll('button[data-up]').forEach((btn) => { btn.addEventListener('click', async () => { const kind = btn.dataset.up; const input = $(kind + '-file'); if (!input.files || !input.files[0]) return toast('Keine Datei gewählt'); const fd = new FormData(); fd.append('image', input.files[0]); const res = await fetch(`/api/admin/images/${kind}`, { method: 'POST', body: fd }); if (res.ok) { toast('Hochgeladen'); input.value = ''; loadImages(); } else { const d = await res.json().catch(() => ({})); toast(d.error || 'Upload fehlgeschlagen'); } }); }); // ---------- Rangliste ---------- document.querySelectorAll('button[data-reset]').forEach((btn) => { btn.addEventListener('click', async () => { const scope = btn.dataset.reset; if (!confirm(scope === 'all' ? 'WIRKLICH die komplette Rangliste leeren?' : 'Heutige Rangliste leeren?')) return; const { ok, data } = await api('/api/admin/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scope }) }); toast(ok ? `Geleert (${data.removed} Einträge)` : 'Fehler'); }); }); // ---------- Init ---------- (async () => { const { ok } = await api('/api/admin/me'); if (ok) showPanel(); else showLogin(); })();