Erster Commit
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
<!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">← 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>
|
||||
Reference in New Issue
Block a user