Files
bordervillegame/admin_settings.html
T
2026-08-22 08:40:29 +02:00

211 lines
8.9 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Einstellungen</title>
<style>
body {
margin: 0;
background: #111;
color: #eee;
font-family: Arial, sans-serif;
padding: 20px;
}
h1 { margin-top: 0; }
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
a.back:hover { text-decoration: underline; }
.panel {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
max-width: 700px;
}
table { width: 100%; border-collapse: collapse; }
th, td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid #333;
font-size: 14px;
vertical-align: middle;
}
th { color: #aaa; font-weight: normal; }
.setting-desc { color: #888; font-size: 12px; margin-top: 2px; }
input {
background: #222;
border: 1px solid #444;
color: #eee;
padding: 6px 8px;
border-radius: 4px;
font-size: 14px;
width: 100px;
box-sizing: border-box;
}
button {
background: #2c7a3d;
border: none;
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
button:hover { opacity: 0.85; }
.default-hint { color: #666; font-size: 11px; }
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
</style>
</head>
<body>
<a class="back" href="/index.html">&larr; zurück zur Startseite</a>
<h1>⚙️ Einstellungen</h1>
<div class="panel">
<p style="color:#888; font-size:13px; margin-top:0;">
Änderungen wirken sofort auf dem laufenden Server, kein Neustart nötig.
</p>
<table>
<thead><tr><th>Einstellung</th><th>Wert</th><th></th></tr></thead>
<tbody id="settingsTableBody"></tbody>
</table>
<div class="msg" id="settingsMsg"></div>
</div>
<script>
const token = localStorage.getItem("token");
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
let isAdmin = localStorage.getItem("isAdmin") === "true";
if (!token) {
alert("Bitte zuerst einloggen.");
location.href = "/index.html";
}
async function authFetch(url, options = {}) {
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
const res = await fetch(url, options);
if (res.status === 401 || res.status === 403) {
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
location.href = "/index.html";
throw new Error("Nicht autorisiert");
}
return res;
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
const SETTING_LABELS = {
salary_interval_minutes: { name: "Gehalts-Intervall", desc: "Alle wie viele Minuten wird Gehalt ausgezahlt", unit: "Min." },
hunger_decay_per_tick: { name: "Hunger-Abnahme", desc: "Abnahme pro Sekunde (kleiner = langsamer hungrig)", unit: "" },
thirst_decay_per_tick: { name: "Durst-Abnahme", desc: "Abnahme pro Sekunde (kleiner = langsamer durstig)", unit: "" },
health_regen_per_tick: { name: "Leben-Regeneration", desc: "Zunahme pro Sekunde, wenn Hunger UND Durst über 50 sind", unit: "" },
health_decay_per_tick: { name: "Leben-Verfall", desc: "Abnahme pro Sekunde, wenn Hunger ODER Durst unter 50 sind", unit: "" },
shop_owner_cut_percent: { name: "Shop-Besitzer-Anteil", desc: "Prozent-Anteil je Verkauf für den Shop-Besitzer", unit: "%" },
gas_owner_cut_percent: { name: "Tankstellen-Besitzer-Anteil", desc: "Prozent-Anteil je Betankung für den Besitzer", unit: "%" },
tow_fee: { name: "Abschlepp-Belohnung", desc: "Betrag pro abgeliefertem Fahrzeug am Abschlepphof", unit: "$" },
car_collision_damage_factor: { name: "Auto-Kollisionsschaden", desc: "Schaden pro Geschwindigkeitseinheit bei Auto-gegen-Auto-Kollision", unit: "" },
pedestrian_damage_factor: { name: "Fußgänger-Kollisionsschaden", desc: "Schaden pro Geschwindigkeitseinheit, wenn ein Auto einen Fußgänger anfährt", unit: "" },
insurance_fee_interval_minutes: { name: "Versicherung: Intervall", desc: "Alle wie viele Minuten wird die Versicherungsgebühr abgebucht", unit: "Min." },
insurance_fee_amount: { name: "Versicherung: Gebühr", desc: "Betrag pro Abbuchung und versichertem Auto", unit: "$" },
insurance_repair_discount_percent: { name: "Versicherung: Rabatt bei Totalschaden", desc: "Prozent-Rabatt auf die Reparatur, wenn das Auto bei 0 Zustand repariert wird", unit: "%" },
tax_interval_minutes: { name: "Steuer: Intervall", desc: "Alle wie viele Minuten wird Steuer abgezogen", unit: "Min." },
tax_percent: { name: "Steuer: Satz", desc: "Prozent vom Bank-Guthaben, der bei jedem Intervall abgezogen wird", unit: "%" },
tuning_cost_per_level: { name: "Tuning: Kosten pro Stufe", desc: "Basiskosten, steigt mit jeder weiteren Stufe (Stufe 2 kostet doppelt usw.)", unit: "$" },
tuning_bonus_per_level_percent: { name: "Tuning: Bonus pro Stufe", desc: "Wie viel Prozent mehr Leistung pro Tuning-Stufe", unit: "%" },
tuning_max_level: { name: "Tuning: Maximalstufe", desc: "Wie viele Stufen pro Kategorie (Motor/Beschleunigung/Bremsen) möglich sind", unit: "" },
tuning_paint_cost: { name: "Tuning: Lackierungs-Preis", desc: "Kosten für eine neue Lackierung", unit: "$" },
npc_traffic_enabled: { name: "NPC-Verkehr", desc: "NPC-Autos serverweit an/aus - wirkt sofort, kein Neustart nötig", unit: "", boolean: true },
cargo_drop_enabled: { name: "Zufalls-Funde", desc: "LKW-Ladungs-Zufallsfunde auf Straßen an/aus", unit: "", boolean: true },
cargo_drop_interval_minutes: { name: "Zufalls-Funde: Intervall", desc: "Alle wie viele Minuten wird die Chance auf einen Fund geprüft", unit: "Min." },
cargo_drop_chance_percent: { name: "Zufalls-Funde: Chance", desc: "Wahrscheinlichkeit pro Intervall, dass wirklich einer spawnt", unit: "%" },
cargo_drop_expiry_minutes: { name: "Zufalls-Funde: Ablaufzeit", desc: "Nach wie vielen Minuten ein nicht abgeholter Fund verschwindet", unit: "Min." }
};
async function loadSettings() {
const res = await authFetch("/api/admin/settings");
const data = await res.json();
const currentValues = {};
(data.settings || []).forEach(s => { currentValues[s.setting_key] = s.setting_value; });
const allKeys = Object.keys(data.defaults || {});
const body = document.getElementById("settingsTableBody");
body.innerHTML = "";
allKeys.forEach(key => {
const label = SETTING_LABELS[key] || { name: key, desc: "", unit: "" };
const value = currentValues[key] !== undefined ? currentValues[key] : data.defaults[key];
const inputHtml = label.boolean
? `<input type="checkbox" class="settingInput" data-key="${key}" data-boolean="true" ${Number(value) ? "checked" : ""} style="width:20px; height:20px;">`
: `<input type="text" class="settingInput" data-key="${key}" value="${escapeHtml(String(value))}">
${label.unit ? escapeHtml(label.unit) : ""}`;
const tr = document.createElement("tr");
tr.innerHTML = `
<td>
<strong>${escapeHtml(label.name)}</strong>
<div class="setting-desc">${escapeHtml(label.desc)}</div>
<div class="default-hint">Standard: ${label.boolean ? (Number(data.defaults[key]) ? "an" : "aus") : data.defaults[key]}</div>
</td>
<td>${inputHtml}</td>
<td><button class="saveBtn" data-key="${key}">Speichern</button></td>
`;
body.appendChild(tr);
});
body.querySelectorAll(".saveBtn").forEach(btn => {
btn.onclick = () => saveSetting(btn.dataset.key);
});
}
async function saveSetting(key) {
const input = document.querySelector(`.settingInput[data-key="${key}"]`);
const isBoolean = input.dataset.boolean === "true";
const value = isBoolean ? (input.checked ? "1" : "0") : input.value.trim();
if (!isBoolean && (value === "" || isNaN(Number(value)))) {
showMsg("Bitte eine gültige Zahl eingeben.", true);
return;
}
const res = await authFetch("/api/admin/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, value })
});
const data = await res.json();
if (data.ok) {
showMsg("Gespeichert - wirkt sofort.");
} else {
showMsg("Fehler: " + (data.error || "unbekannt"), true);
}
}
function showMsg(text, isError) {
const el = document.getElementById("settingsMsg");
el.style.color = isError ? "#e06c6c" : "#6fbf73";
el.textContent = text;
setTimeout(() => { el.textContent = ""; }, 4000);
}
loadSettings();
</script>
</body>
</html>