Erster Commit

This commit is contained in:
2026-08-22 08:40:29 +02:00
commit 875477d425
1961 changed files with 930336 additions and 0 deletions
+752
View File
@@ -0,0 +1,752 @@
<!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">&larr; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>