752 lines
30 KiB
HTML
752 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Übersicht</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: #111;
|
|
color: #eee;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
a { color: #6fbf73; }
|
|
header {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 24px 20px 0;
|
|
}
|
|
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
|
a.back:hover { text-decoration: underline; }
|
|
h1 { margin: 10px 0 24px; }
|
|
|
|
main { max-width: 900px; margin: 0 auto; padding: 0 20px 40px; }
|
|
|
|
section.panel {
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
section.panel h2 {
|
|
margin-top: 0;
|
|
border-bottom: 2px solid #333;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.news-columns { display: flex; gap: 24px; align-items: flex-start; }
|
|
.news-columns section.panel { flex: 1; min-width: 0; }
|
|
@media (max-width: 800px) {
|
|
.news-columns { flex-direction: column; }
|
|
}
|
|
|
|
.news-item { border-bottom: 1px solid #2a2a2a; padding: 14px 0; }
|
|
.news-item:last-child { border-bottom: none; }
|
|
.news-item h3 { margin: 0 0 4px 0; }
|
|
.news-meta { color: #777; font-size: 12px; margin-bottom: 8px; }
|
|
.news-content { white-space: pre-wrap; line-height: 1.5; }
|
|
#newsEmpty, #changelogEmpty { color: #666; }
|
|
|
|
.admin-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
.admin-card {
|
|
background: #202020;
|
|
border: 1px solid #333;
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
text-decoration: none;
|
|
color: #eee;
|
|
display: block;
|
|
transition: border-color 0.15s;
|
|
}
|
|
.admin-card:hover { border-color: #2c7a3d; }
|
|
.admin-card .icon { font-size: 26px; margin-bottom: 8px; }
|
|
.admin-card h3 { margin: 0 0 4px 0; font-size: 16px; }
|
|
.admin-card p { margin: 0; color: #999; font-size: 13px; }
|
|
|
|
.links-row { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 14px; }
|
|
.links-row a { text-decoration: none; }
|
|
.links-row a:hover { text-decoration: underline; }
|
|
|
|
.form-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
|
input, textarea {
|
|
background: #222; border: 1px solid #444; color: #eee;
|
|
padding: 8px 10px; border-radius: 4px; font-size: 14px; font-family: inherit;
|
|
}
|
|
textarea { width: 100%; min-height: 100px; box-sizing: border-box; resize: vertical; }
|
|
button {
|
|
background: #2c7a3d; border: none; color: white; padding: 8px 14px;
|
|
border-radius: 4px; cursor: pointer; font-size: 14px;
|
|
}
|
|
button.secondary { background: #555; }
|
|
button:hover { opacity: 0.9; }
|
|
|
|
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 6px; }
|
|
.hidden { display: none !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<a class="back" href="/index.html">← Startseite</a>
|
|
<h1>📂 Übersicht</h1>
|
|
</header>
|
|
|
|
<main>
|
|
<!-- ADMIN BEREICH (nur sichtbar für Admins) -->
|
|
<section class="panel hidden" id="adminPanel">
|
|
<h2>🛠️ Admin-Bereich</h2>
|
|
<div class="admin-grid">
|
|
<a class="admin-card" href="/admin.html" data-permission="manage_shops_items">
|
|
<div class="icon">📦</div>
|
|
<h3>Items & Shops</h3>
|
|
<p>Items anlegen und Shops verwalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_cars.html" data-permission="manage_vehicles">
|
|
<div class="icon">🚗</div>
|
|
<h3>Fahrzeuge</h3>
|
|
<p>Automodelle, Farben, Fahrverhalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/map_editor.html" data-permission="manage_maps">
|
|
<div class="icon">🗺️</div>
|
|
<h3>Map-Editor</h3>
|
|
<p>Karten, Türen, Objekte, Garagen</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_players.html" data-permission="manage_players">
|
|
<div class="icon">🎒</div>
|
|
<h3>Spieler-Inventare</h3>
|
|
<p>Inventar jedes Spielers bearbeiten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_users.html" data-permission="manage_bans">
|
|
<div class="icon">👥</div>
|
|
<h3>Benutzerverwaltung</h3>
|
|
<p>Accounts freischalten, Admin-Rechte vergeben</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_permissions.html" data-permission="manage_admin_groups">
|
|
<div class="icon">🔑</div>
|
|
<h3>Berechtigungen & Team</h3>
|
|
<p>Gruppen mit abgestuften Rechten für dein Team verwalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_jobs.html" data-permission="manage_jobs">
|
|
<div class="icon">💼</div>
|
|
<h3>Jobs & Ränge</h3>
|
|
<p>Jobs, Ränge und Gehälter verwalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_tiles.html" data-permission="manage_tiles_objects">
|
|
<div class="icon">🧱</div>
|
|
<h3>Tiles & Objekte</h3>
|
|
<p>Tile- und Objekt-Typen verwalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_points.html" data-permission="manage_locations,manage_content,manage_discord,manage_maintenance,server_restart">
|
|
<div class="icon">📍</div>
|
|
<h3>Orte & Inhalte</h3>
|
|
<p>Alle Karten-Punkte, News und Changelog verwalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_logs.html" data-permission="manage_logs">
|
|
<div class="icon">📋</div>
|
|
<h3>Logs</h3>
|
|
<p>Bans, Verbrechen, Admin-Aktionen und mehr einsehen</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_radio.html" data-permission="manage_radio">
|
|
<div class="icon">📻</div>
|
|
<h3>Radiosender</h3>
|
|
<p>Sender für das Auto-Radio verwalten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_drugs.html" data-permission="manage_drugs">
|
|
<div class="icon">🌿</div>
|
|
<h3>Drogensorten</h3>
|
|
<p>Sorten anlegen und mit Items verknüpfen</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_settings.html" data-permission="manage_settings">
|
|
<div class="icon">⚙️</div>
|
|
<h3>Einstellungen</h3>
|
|
<p>Gehalt, Hunger/Durst, Besitzer-Anteile, Abschlepp-Gebühr</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_events.html" data-permission="manage_events">
|
|
<div class="icon">🎉</div>
|
|
<h3>Server-Events</h3>
|
|
<p>Doppel-XP und Rabatt-Aktionen auslösen</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_clothing.html" data-permission="manage_clothing">
|
|
<div class="icon">👕</div>
|
|
<h3>Kleidungs-Editor</h3>
|
|
<p>Oberteile, Hosen, Schuhe und Helme bemalen</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_skins.html" data-permission="manage_clothing">
|
|
<div class="icon">🎨</div>
|
|
<h3>Skin-Editor</h3>
|
|
<p>Eigene Skins pixelgenau bemalen</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_tickets.html" data-permission="manage_tickets">
|
|
<div class="icon">🎫</div>
|
|
<h3>Support-Tickets</h3>
|
|
<p>Anliegen der Spieler bearbeiten und beantworten</p>
|
|
</a>
|
|
<a class="admin-card" href="/admin_metrics.html" data-permission="view_metrics">
|
|
<div class="icon">📊</div>
|
|
<h3>Server-Metriken</h3>
|
|
<p>Spielerzahl, Tick-Dauer und RAM über die Zeit</p>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="server_restart">
|
|
<h3 style="margin-top:24px;">🔄 Server-Neustart</h3>
|
|
<p style="color:#888; font-size:13px; margin-top:-4px;">
|
|
Alle Spieler werden per Countdown gewarnt und automatisch sicher getrennt, bevor der Server neu startet.
|
|
</p>
|
|
<div class="form-row">
|
|
<input type="number" id="restartSeconds" value="60" min="5" max="600" style="width:100px;">
|
|
<button onclick="startRestart()">Neustart einleiten</button>
|
|
<button class="secondary" onclick="cancelRestart()">Abbrechen</button>
|
|
</div>
|
|
<div class="msg" id="restartMsg"></div>
|
|
|
|
<h4 style="margin-top:20px; margin-bottom:8px; color:#aaa;">Automatischer täglicher Neustart</h4>
|
|
<div class="form-row">
|
|
<label style="display:flex; align-items:center; gap:6px;">
|
|
<input type="checkbox" id="autoRestartEnabled" onchange="saveAutoRestart()">
|
|
Aktiviert
|
|
</label>
|
|
<input type="time" id="autoRestartTime" onchange="saveAutoRestart()">
|
|
</div>
|
|
<div class="msg" id="autoRestartMsg"></div>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="manage_maintenance">
|
|
<h3 style="margin-top:24px;">🔧 Wartungsmodus</h3>
|
|
<p style="color:#888; font-size:13px; margin-top:-4px;">
|
|
Solange aktiv, können sich nur noch Admins einloggen. Bereits verbundene Spieler bleiben im Spiel.
|
|
</p>
|
|
<div class="form-row">
|
|
<span id="maintenanceStatus" style="align-self:center; font-size:14px;">Status wird geladen...</span>
|
|
<button onclick="toggleMaintenance(true)">Aktivieren</button>
|
|
<button class="secondary" onclick="toggleMaintenance(false)">Deaktivieren</button>
|
|
</div>
|
|
<div class="msg" id="maintenanceMsg"></div>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="manage_beta_access">
|
|
<h3 style="margin-top:24px;">🧪 Beta-Zugang</h3>
|
|
<p style="color:#888; font-size:13px; margin-top:-4px;">
|
|
Solange aktiv, können sich nur noch Admins und als Beta-Tester markierte Spieler einloggen.
|
|
</p>
|
|
<div class="form-row">
|
|
<span id="betaModeStatus" style="align-self:center; font-size:14px;">Status wird geladen...</span>
|
|
<button onclick="toggleBetaMode(true)">Aktivieren</button>
|
|
<button class="secondary" onclick="toggleBetaMode(false)">Deaktivieren</button>
|
|
</div>
|
|
<div class="msg" id="betaModeMsg"></div>
|
|
|
|
<h4 style="margin-top:16px;">Beta-Tester verwalten</h4>
|
|
<div class="form-row">
|
|
<input type="text" id="betaTesterUsernameInput" placeholder="Benutzername">
|
|
<button onclick="addBetaTester()">Hinzufügen</button>
|
|
</div>
|
|
<div id="betaTesterList" style="margin-top:10px;"></div>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="manage_admin_groups">
|
|
<h3 style="margin-top:24px;">↩️ Löschung rückgängig machen</h3>
|
|
<p style="color:#888; font-size:13px; margin-top:-4px;">
|
|
Zeigt die letzten 10 Admin-Löschungen (egal aus welchem Bereich - Standorte, Items, Jobs usw.). Stellt die komplette Zeile wieder her, sofern es dort keinen Namens-/ID-Konflikt gibt.
|
|
</p>
|
|
<button onclick="loadLastDeletions()">Aktualisieren</button>
|
|
<div id="deletionsList" style="margin-top:10px;"></div>
|
|
<div class="msg" id="undoMsg"></div>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="manage_settings,manage_content">
|
|
<h3 style="margin-top:24px;">Webseiten-Titel</h3>
|
|
<input id="siteTitleInput" placeholder="z.B. Mein Multiplayer-Spiel" style="width:100%; box-sizing:border-box; margin-bottom:8px;">
|
|
<div class="form-row">
|
|
<button onclick="saveSiteTitle()">Speichern</button>
|
|
</div>
|
|
<div class="msg" id="siteTitleMsg"></div>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="manage_content">
|
|
<h3 style="margin-top:24px;">News verfassen</h3>
|
|
<input id="newsTitle" placeholder="Titel" style="width:100%; box-sizing:border-box; margin-bottom:8px;">
|
|
<textarea id="newsContent" placeholder="Inhalt..."></textarea>
|
|
<div class="form-row">
|
|
<button onclick="postNews()">Veröffentlichen</button>
|
|
</div>
|
|
<div class="msg" id="newsMsg"></div>
|
|
</div>
|
|
|
|
<div class="perm-section" data-permission="manage_content">
|
|
<h3 style="margin-top:24px;">Changelog-Eintrag verfassen</h3>
|
|
<div class="form-row" style="margin-top:0;">
|
|
<input id="changelogVersion" placeholder="Version (z.B. 1.1.0)" style="width:160px;">
|
|
</div>
|
|
<input id="changelogTitle" placeholder="Titel" style="width:100%; box-sizing:border-box; margin:8px 0;">
|
|
<textarea id="changelogContent" placeholder="Was hat sich geändert..."></textarea>
|
|
<div class="form-row">
|
|
<button onclick="postChangelog()">Veröffentlichen</button>
|
|
</div>
|
|
<div class="msg" id="changelogMsg"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- WEITERE SEITEN (für alle sichtbar) -->
|
|
<section class="panel">
|
|
<h2>🔗 Weitere Seiten</h2>
|
|
<div class="links-row">
|
|
<a href="/wuensche.html">Wünsche</a>
|
|
<a href="/umfragen.html">Umfragen</a>
|
|
<a href="/bestenliste.html">Bestenliste</a>
|
|
<a href="/auktionshaus.html">Auktionshaus</a>
|
|
<a href="/anmerkungen.html">Anmerkungen</a>
|
|
<a href="/impressum.html" style="color:#888;">Impressum</a>
|
|
<a href="/datenschutz.html" style="color:#888;">Datenschutz</a>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- NEWS + CHANGELOG nebeneinander (für alle sichtbar) -->
|
|
<div class="news-columns">
|
|
<section class="panel">
|
|
<h2>📰 News</h2>
|
|
<div id="newsList"></div>
|
|
<div id="newsEmpty" class="hidden">Noch keine News vorhanden.</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>🛠️ Changelog</h2>
|
|
<div id="changelogList"></div>
|
|
<div id="changelogEmpty" class="hidden">Noch keine Einträge vorhanden.</div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
let token = localStorage.getItem("token") || null;
|
|
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
|
let myPermissions = [];
|
|
|
|
// -------------------------------------------------------------
|
|
// ADMIN-BEREICH: nur die Kacheln zeigen, für die tatsächlich eine
|
|
// Berechtigung vorliegt (Voll-Admins sehen alles)
|
|
// -------------------------------------------------------------
|
|
async function loadMyPermissionsAndShowAdminPanel() {
|
|
if (!token) return;
|
|
|
|
try {
|
|
const res = await fetch("/api/my_permissions", { headers: { "Authorization": "Bearer " + token } });
|
|
const data = await res.json();
|
|
if (!data.ok) return;
|
|
|
|
isAdmin = data.isAdmin;
|
|
myPermissions = data.permissions || [];
|
|
|
|
if (!isAdmin && myPermissions.length === 0) return; // keinerlei Berechtigung - Admin-Bereich bleibt versteckt
|
|
|
|
document.querySelectorAll(".admin-card, .perm-section").forEach(card => {
|
|
const needed = (card.dataset.permission || "").split(",").filter(Boolean);
|
|
const allowed = isAdmin || needed.some(key => myPermissions.includes(key));
|
|
card.style.display = allowed ? "" : "none";
|
|
});
|
|
|
|
document.getElementById("adminPanel").classList.remove("hidden");
|
|
} catch (err) {
|
|
console.error("Berechtigungen konnten nicht geladen werden:", err);
|
|
}
|
|
}
|
|
loadMyPermissionsAndShowAdminPanel();
|
|
|
|
// -------------------------------------------------------------
|
|
// SEITEN-TITEL
|
|
// -------------------------------------------------------------
|
|
async function loadSiteTitle() {
|
|
try {
|
|
const res = await fetch("/api/site_config");
|
|
const data = await res.json();
|
|
if (data.ok && data.title) {
|
|
document.title = "Übersicht - " + data.title;
|
|
const input = document.getElementById("siteTitleInput");
|
|
if (input && !input.value) input.value = data.title;
|
|
}
|
|
} catch (err) {
|
|
console.error("Seiten-Titel konnte nicht geladen werden:", err);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// SERVER-NEUSTART
|
|
// -------------------------------------------------------------
|
|
// -------------------------------------------------------------
|
|
// LÖSCHUNG RÜCKGÄNGIG MACHEN
|
|
// -------------------------------------------------------------
|
|
async function loadLastDeletions() {
|
|
const res = await fetch("/api/admin/last_deletions", {
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
const el = document.getElementById("deletionsList");
|
|
|
|
if (!data.ok || data.deletions.length === 0) {
|
|
el.innerHTML = `<p style="color:#888; font-size:13px;">Keine protokollierten Löschungen vorhanden.</p>`;
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = data.deletions.map(d => {
|
|
const row = typeof d.row_data === "string" ? JSON.parse(d.row_data) : d.row_data;
|
|
const preview = row.name || row.username || row.model || row.symbol || `ID ${row.id ?? "?"}`;
|
|
const when = new Date(d.deleted_at).toLocaleString("de-DE");
|
|
return `
|
|
<div class="form-row" style="justify-content:space-between; padding:6px 0; border-bottom:1px solid #333;">
|
|
<span style="font-size:13px;"><b>${escapeHtml(d.table_name)}</b>: ${escapeHtml(String(preview))} <span style="color:#888;">(${when})</span></span>
|
|
<button onclick="undoDeletion(${d.id})">Wiederherstellen</button>
|
|
</div>`;
|
|
}).join("");
|
|
}
|
|
|
|
async function undoDeletion(logId) {
|
|
const res = await fetch("/api/admin/undo_last_delete", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ logId })
|
|
});
|
|
const data = await res.json();
|
|
const msgEl = document.getElementById("undoMsg");
|
|
|
|
if (data.ok) {
|
|
msgEl.style.color = "#6fbf73";
|
|
msgEl.textContent = `✅ Wiederhergestellt in "${data.table}".`;
|
|
loadLastDeletions();
|
|
} else {
|
|
msgEl.style.color = "#e06c6c";
|
|
msgEl.textContent = "❌ " + data.error;
|
|
}
|
|
}
|
|
|
|
async function startRestart() {
|
|
const seconds = Number(document.getElementById("restartSeconds").value) || 60;
|
|
|
|
const res = await fetch("/api/admin/restart_server", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ seconds })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
showMsg("restartMsg", `Neustart in ${data.seconds}s eingeleitet.`);
|
|
} else {
|
|
showMsg("restartMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
|
}
|
|
}
|
|
|
|
async function cancelRestart() {
|
|
const res = await fetch("/api/admin/restart_server", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ cancel: true })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
showMsg("restartMsg", data.cancelled ? "Neustart abgebrochen." : "Es lief gerade kein Neustart-Countdown.");
|
|
} else {
|
|
showMsg("restartMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
|
}
|
|
}
|
|
|
|
async function loadAutoRestart() {
|
|
const res = await fetch("/api/admin/auto_restart", { headers: { "Authorization": "Bearer " + token } });
|
|
const data = await res.json();
|
|
if (!data.ok) return;
|
|
|
|
document.getElementById("autoRestartEnabled").checked = data.enabled;
|
|
document.getElementById("autoRestartTime").value = data.time;
|
|
}
|
|
|
|
async function saveAutoRestart() {
|
|
const enabled = document.getElementById("autoRestartEnabled").checked;
|
|
const time = document.getElementById("autoRestartTime").value;
|
|
|
|
const res = await fetch("/api/admin/auto_restart", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ enabled, time })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
showMsg("autoRestartMsg", enabled ? `Aktiviert - täglich um ${data.time} Uhr.` : "Deaktiviert.");
|
|
} else {
|
|
showMsg("autoRestartMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// WARTUNGSMODUS
|
|
// -------------------------------------------------------------
|
|
async function loadMaintenanceStatus() {
|
|
const res = await fetch("/api/admin/maintenance_mode", {
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
const el = document.getElementById("maintenanceStatus");
|
|
if (data.ok) {
|
|
el.textContent = data.active ? "🔴 Aktiv (Logins gesperrt)" : "🟢 Inaktiv";
|
|
} else {
|
|
el.textContent = "Status unbekannt";
|
|
}
|
|
}
|
|
|
|
async function toggleMaintenance(active) {
|
|
const res = await fetch("/api/admin/maintenance_mode", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ active })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
showMsg("maintenanceMsg", active ? "Wartungsmodus aktiviert." : "Wartungsmodus deaktiviert.");
|
|
loadMaintenanceStatus();
|
|
} else {
|
|
showMsg("maintenanceMsg", "Fehler beim Umschalten.", true);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// BETA-ZUGANG
|
|
// -------------------------------------------------------------
|
|
async function loadBetaModeStatus() {
|
|
const res = await fetch("/api/admin/beta_mode", {
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
const el = document.getElementById("betaModeStatus");
|
|
if (data.ok) {
|
|
el.textContent = data.active ? "🔴 Aktiv (nur Beta-Tester + Admins)" : "🟢 Inaktiv (alle können sich einloggen)";
|
|
} else {
|
|
el.textContent = "Status unbekannt";
|
|
}
|
|
}
|
|
|
|
async function toggleBetaMode(active) {
|
|
const res = await fetch("/api/admin/beta_mode", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ active })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
showMsg("betaModeMsg", active ? "Beta-Modus aktiviert." : "Beta-Modus deaktiviert.");
|
|
loadBetaModeStatus();
|
|
} else {
|
|
showMsg("betaModeMsg", "Fehler beim Umschalten.", true);
|
|
}
|
|
}
|
|
|
|
async function loadBetaTesters() {
|
|
const res = await fetch("/api/admin/beta_testers", {
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
const el = document.getElementById("betaTesterList");
|
|
if (!data.ok || data.testers.length === 0) {
|
|
el.innerHTML = `<p style="color:#888; font-size:13px;">Noch keine Beta-Tester markiert.</p>`;
|
|
return;
|
|
}
|
|
el.innerHTML = data.testers.map(t => `
|
|
<div class="form-row" style="justify-content:space-between; padding:4px 0; border-bottom:1px solid #333;">
|
|
<span style="font-size:13px;">${escapeHtml(t.username)}</span>
|
|
<button class="secondary" onclick="removeBetaTester('${escapeHtml(t.username)}')">Entfernen</button>
|
|
</div>`).join("");
|
|
}
|
|
|
|
async function addBetaTester() {
|
|
const username = document.getElementById("betaTesterUsernameInput").value.trim();
|
|
if (!username) return;
|
|
await setBetaTester(username, true);
|
|
document.getElementById("betaTesterUsernameInput").value = "";
|
|
}
|
|
async function removeBetaTester(username) {
|
|
await setBetaTester(username, false);
|
|
}
|
|
async function setBetaTester(username, isBetaTester) {
|
|
const res = await fetch("/api/admin/set_beta_tester", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ username, isBetaTester })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
showMsg("betaModeMsg", isBetaTester ? `${username} als Beta-Tester hinzugefügt.` : `${username} entfernt.`);
|
|
loadBetaTesters();
|
|
} else {
|
|
showMsg("betaModeMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
|
}
|
|
}
|
|
|
|
async function saveSiteTitle() {
|
|
const title = document.getElementById("siteTitleInput").value.trim();
|
|
if (!title) {
|
|
showMsg("siteTitleMsg", "Titel darf nicht leer sein.", true);
|
|
return;
|
|
}
|
|
const res = await fetch("/api/admin/site_config", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ title })
|
|
});
|
|
const data = await res.json();
|
|
showMsg("siteTitleMsg", data.ok ? "Gespeichert." : "Fehler: " + (data.error || "unbekannt"), !data.ok);
|
|
if (data.ok) loadSiteTitle();
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// NEWS
|
|
// -------------------------------------------------------------
|
|
async function loadNews() {
|
|
const res = await fetch("/api/news");
|
|
const data = await res.json();
|
|
const list = document.getElementById("newsList");
|
|
const empty = document.getElementById("newsEmpty");
|
|
|
|
if (!data.news || data.news.length === 0) {
|
|
empty.classList.remove("hidden");
|
|
list.innerHTML = "";
|
|
return;
|
|
}
|
|
empty.classList.add("hidden");
|
|
|
|
list.innerHTML = data.news.map(n => `
|
|
<div class="news-item">
|
|
<h3>${escapeHtml(n.title)}</h3>
|
|
<div class="news-meta">${escapeHtml(n.author)} · ${new Date(n.created_at).toLocaleString("de-DE")}</div>
|
|
<div class="news-content">${escapeHtml(n.content)}</div>
|
|
${isAdmin ? `<button class="danger" style="background:#a83232; margin-top:8px; font-size:12px; padding:4px 10px;" onclick="deleteNews(${n.id})">Löschen</button>` : ""}
|
|
</div>
|
|
`).join("");
|
|
}
|
|
|
|
async function postNews() {
|
|
const title = document.getElementById("newsTitle").value.trim();
|
|
const content = document.getElementById("newsContent").value.trim();
|
|
if (!title || !content) {
|
|
showMsg("newsMsg", "Titel und Inhalt sind Pflicht.", true);
|
|
return;
|
|
}
|
|
const res = await fetch("/api/admin/news", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ title, content })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
showMsg("newsMsg", "Veröffentlicht.");
|
|
document.getElementById("newsTitle").value = "";
|
|
document.getElementById("newsContent").value = "";
|
|
loadNews();
|
|
} else {
|
|
showMsg("newsMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
|
}
|
|
}
|
|
|
|
async function deleteNews(id) {
|
|
if (!confirm("Diese News wirklich löschen?")) return;
|
|
const res = await fetch("/api/admin/news/" + id, {
|
|
method: "DELETE",
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) loadNews();
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// CHANGELOG
|
|
// -------------------------------------------------------------
|
|
async function loadChangelog() {
|
|
const res = await fetch("/api/changelog");
|
|
const data = await res.json();
|
|
const list = document.getElementById("changelogList");
|
|
const empty = document.getElementById("changelogEmpty");
|
|
|
|
if (!data.changelog || data.changelog.length === 0) {
|
|
empty.classList.remove("hidden");
|
|
list.innerHTML = "";
|
|
return;
|
|
}
|
|
empty.classList.add("hidden");
|
|
|
|
list.innerHTML = data.changelog.map(c => `
|
|
<div class="news-item">
|
|
<h3>${escapeHtml(c.version)} - ${escapeHtml(c.title)}</h3>
|
|
<div class="news-meta">${new Date(c.created_at).toLocaleString("de-DE")}</div>
|
|
<div class="news-content">${escapeHtml(c.content)}</div>
|
|
${isAdmin ? `<button class="danger" style="background:#a83232; margin-top:8px; font-size:12px; padding:4px 10px;" onclick="deleteChangelog(${c.id})">Löschen</button>` : ""}
|
|
</div>
|
|
`).join("");
|
|
}
|
|
|
|
async function postChangelog() {
|
|
const version = document.getElementById("changelogVersion").value.trim();
|
|
const title = document.getElementById("changelogTitle").value.trim();
|
|
const content = document.getElementById("changelogContent").value.trim();
|
|
if (!version || !title || !content) {
|
|
showMsg("changelogMsg", "Version, Titel und Inhalt sind Pflicht.", true);
|
|
return;
|
|
}
|
|
const res = await fetch("/api/admin/changelog", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token },
|
|
body: JSON.stringify({ version, title, content })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
showMsg("changelogMsg", "Veröffentlicht.");
|
|
document.getElementById("changelogVersion").value = "";
|
|
document.getElementById("changelogTitle").value = "";
|
|
document.getElementById("changelogContent").value = "";
|
|
loadChangelog();
|
|
} else {
|
|
showMsg("changelogMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
|
}
|
|
}
|
|
|
|
async function deleteChangelog(id) {
|
|
if (!confirm("Diesen Changelog-Eintrag wirklich löschen?")) return;
|
|
const res = await fetch("/api/admin/changelog/" + id, {
|
|
method: "DELETE",
|
|
headers: { "Authorization": "Bearer " + token }
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) loadChangelog();
|
|
}
|
|
|
|
function showMsg(elId, text, isError) {
|
|
const el = document.getElementById(elId);
|
|
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
|
el.textContent = text;
|
|
setTimeout(() => { el.textContent = ""; }, 4000);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text == null ? "" : String(text);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// INIT
|
|
// -------------------------------------------------------------
|
|
loadSiteTitle();
|
|
loadNews();
|
|
loadChangelog();
|
|
if (token && isAdmin) loadMaintenanceStatus();
|
|
if (token && isAdmin) { loadBetaModeStatus(); loadBetaTesters(); }
|
|
if (token && isAdmin) loadLastDeletions();
|
|
if (token && isAdmin) loadAutoRestart();
|
|
</script>
|
|
</body>
|
|
</html> |