feat: Servereinstellungen-Seite für Admins
- Neuer Endpoint GET/PUT /api/admin/server-settings - Liest appsettings.Production.json (Fallback: appsettings.json → Standardwert) - Schreibt immer in appsettings.Production.json - Definierte Settings: AnthropicToken, MaxIterations, PayloadLogging, PayloadLoggingGzip, KreaToken, KreaApiKey - Neue Frontend-Seite 'server-settings' (Admin-only) - Dynamisches Formular basierend auf Setting-Definitionen vom Backend - Password-Felder mit Eye-Toggle, Checkboxen für Booleans, Number-Inputs - Hinweis dass Neustart erforderlich ist - Dashboard: Neuer Link 'Servereinstellungen' für Admins - appsettings.Production.json vom Publish ausgeschlossen (CopyToPublishDirectory=Never)
This commit is contained in:
1 parent
af2f821c72
commit
bfd78278ab
8 files changed
+411
-3
No files matched your search
@@ -0,0 +1,176 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Gott.Endpoints;
|
||||||
|
|
||||||
|
public static class ServerSettingsEndpoint
|
||||||
|
{
|
||||||
|
private static readonly string ProductionConfigPath =
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "appsettings.Production.json");
|
||||||
|
|
||||||
|
private static readonly string FallbackConfigPath =
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||||
|
|
||||||
|
// Definiert alle konfigurierbaren Properties mit Typ und Standardwert
|
||||||
|
private static readonly List<SettingDefinition> Definitions = new List<SettingDefinition>
|
||||||
|
{
|
||||||
|
new SettingDefinition("AnthropicToken", "string", "", "Anthropic API Key", "password"),
|
||||||
|
new SettingDefinition("MaxIterations", "int", 100, "Max Tool-Loop Iterationen pro Chat", "number"),
|
||||||
|
new SettingDefinition("PayloadLogging", "bool", false, "API-Payloads loggen (Debug)", "toggle"),
|
||||||
|
new SettingDefinition("PayloadLoggingGzip", "bool", false, "Payload-Logs als .gz komprimieren", "toggle"),
|
||||||
|
new SettingDefinition("KreaToken", "string", "", "Krea API Token", "password"),
|
||||||
|
new SettingDefinition("KreaApiKey", "string", "", "API-Key für Krea-Endpoints (Automation)", "password"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void MapServerSettingsEndpoints(this WebApplication app)
|
||||||
|
{
|
||||||
|
app.MapGet("/api/admin/server-settings", HandleGet);
|
||||||
|
app.MapPut("/api/admin/server-settings", HandleUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleGet(HttpContext ctx, IConfiguration config)
|
||||||
|
{
|
||||||
|
if (!ctx.IsAdmin())
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 403;
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production-Config lesen (falls vorhanden)
|
||||||
|
JsonObject prodConfig = null;
|
||||||
|
if (File.Exists(ProductionConfigPath))
|
||||||
|
{
|
||||||
|
var json = await File.ReadAllTextAsync(ProductionConfigPath);
|
||||||
|
prodConfig = JsonNode.Parse(json) as JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback-Config lesen
|
||||||
|
JsonObject fallbackConfig = null;
|
||||||
|
if (File.Exists(FallbackConfigPath))
|
||||||
|
{
|
||||||
|
var json = await File.ReadAllTextAsync(FallbackConfigPath);
|
||||||
|
fallbackConfig = JsonNode.Parse(json) as JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = new List<object>();
|
||||||
|
|
||||||
|
foreach (var def in Definitions)
|
||||||
|
{
|
||||||
|
object value = def.DefaultValue;
|
||||||
|
|
||||||
|
// Fallback: appsettings.json
|
||||||
|
if (fallbackConfig != null && fallbackConfig.ContainsKey(def.Key))
|
||||||
|
{
|
||||||
|
value = ExtractValue(fallbackConfig[def.Key], def.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override: appsettings.Production.json
|
||||||
|
if (prodConfig != null && prodConfig.ContainsKey(def.Key))
|
||||||
|
{
|
||||||
|
value = ExtractValue(prodConfig[def.Key], def.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Add(new
|
||||||
|
{
|
||||||
|
key = def.Key,
|
||||||
|
type = def.Type,
|
||||||
|
value = value,
|
||||||
|
label = def.Label,
|
||||||
|
inputType = def.InputType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new { settings });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleUpdate(HttpContext ctx)
|
||||||
|
{
|
||||||
|
if (!ctx.IsAdmin())
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 403;
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||||
|
|
||||||
|
// Bestehende Production-Config laden oder neue erstellen
|
||||||
|
JsonObject prodConfig;
|
||||||
|
if (File.Exists(ProductionConfigPath))
|
||||||
|
{
|
||||||
|
var existing = await File.ReadAllTextAsync(ProductionConfigPath);
|
||||||
|
prodConfig = JsonNode.Parse(existing) as JsonObject ?? new JsonObject();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prodConfig = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur definierte Keys akzeptieren
|
||||||
|
foreach (var def in Definitions)
|
||||||
|
{
|
||||||
|
if (!body.TryGetProperty(def.Key, out var prop)) continue;
|
||||||
|
|
||||||
|
// Alten Wert entfernen falls vorhanden
|
||||||
|
prodConfig.Remove(def.Key);
|
||||||
|
|
||||||
|
switch (def.Type)
|
||||||
|
{
|
||||||
|
case "string":
|
||||||
|
prodConfig.Add(def.Key, prop.GetString() ?? "");
|
||||||
|
break;
|
||||||
|
case "int":
|
||||||
|
prodConfig.Add(def.Key, prop.TryGetInt32(out var i) ? i : (int)def.DefaultValue);
|
||||||
|
break;
|
||||||
|
case "bool":
|
||||||
|
prodConfig.Add(def.Key, prop.ValueKind == JsonValueKind.True);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||||
|
var json = prodConfig.ToJsonString(options);
|
||||||
|
await File.WriteAllTextAsync(ProductionConfigPath, json);
|
||||||
|
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new { ok = true, hint = "Die Änderungen erfordern einen Neustart des Servers." });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ExtractValue(JsonNode node, string type)
|
||||||
|
{
|
||||||
|
if (node == null) return null;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "string": return node.GetValue<string>();
|
||||||
|
case "int": return node.GetValue<int>();
|
||||||
|
case "bool": return node.GetValue<bool>();
|
||||||
|
default: return node.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SettingDefinition
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Type { get; }
|
||||||
|
public object DefaultValue { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public string InputType { get; }
|
||||||
|
|
||||||
|
public SettingDefinition(string key, string type, object defaultValue, string label, string inputType)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Type = type;
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
Label = label;
|
||||||
|
InputType = inputType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,5 +82,6 @@ app.MapCronjobEndpoints();
|
|||||||
app.MapKreaEndpoints();
|
app.MapKreaEndpoints();
|
||||||
app.MapSharedEndpoints();
|
app.MapSharedEndpoints();
|
||||||
app.MapAdminEndpoints();
|
app.MapAdminEndpoints();
|
||||||
|
app.MapServerSettingsEndpoints();
|
||||||
|
|
||||||
app.Run("http://0.0.0.0:19000");
|
app.Run("http://0.0.0.0:19000");
|
||||||
@@ -30,6 +30,10 @@
|
|||||||
<Content Include="Templates/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
<Content Include="Templates/**" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="appsettings.Production.json" CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Anthropic" Version="12.8.0" />
|
<PackageReference Include="Anthropic" Version="12.8.0" />
|
||||||
<PackageReference Include="Cronos" Version="0.11.1" />
|
<PackageReference Include="Cronos" Version="0.11.1" />
|
||||||
|
|||||||
+36
-2
@@ -566,11 +566,45 @@ label.skill-item {
|
|||||||
|
|
||||||
.form-hint {
|
.form-hint {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
|
||||||
font-size: var(--font-size);
|
|
||||||
opacity: var(--semi-transparent);
|
opacity: var(--semi-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-hint code {
|
||||||
|
background: var(--f1-bg);
|
||||||
|
padding: var(--badge-padding);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
margin-bottom: var(--box-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field label {
|
||||||
|
margin-top: var(--badge-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field .toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field .toggle-label input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
color: var(--dangerous);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Admin User List ── */
|
/* ── Admin User List ── */
|
||||||
|
|
||||||
.admin-user-list {
|
.admin-user-list {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<div class="dashboard-links">
|
<div class="dashboard-links">
|
||||||
<a id="dashboardSettingsLink" class="dashboard-link">Einstellungen</a>
|
<a id="dashboardSettingsLink" class="dashboard-link">Einstellungen</a>
|
||||||
<a id="dashboardAdminLink" class="dashboard-link" hidden>Benutzerverwaltung</a>
|
<a id="dashboardAdminLink" class="dashboard-link" hidden>Benutzerverwaltung</a>
|
||||||
|
<a id="dashboardServerSettingsLink" class="dashboard-link" hidden>Servereinstellungen</a>
|
||||||
<a id="dashboardProfileLink" class="dashboard-link">Mein Profil</a>
|
<a id="dashboardProfileLink" class="dashboard-link">Mein Profil</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -444,6 +445,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="tpl-server-settings">
|
||||||
|
<div class="page form-page">
|
||||||
|
<header>
|
||||||
|
<button class="back-btn icon-btn" data-page="dashboard"><i class="fa fa-arrow-left"></i></button>
|
||||||
|
<h1>Servereinstellungen</h1>
|
||||||
|
</header>
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="form">
|
||||||
|
<p class="form-hint"><i class="fa fa-info-circle"></i> Änderungen werden in <code>appsettings.Production.json</code> gespeichert. Danach ist ein Neustart erforderlich.</p>
|
||||||
|
<div id="serverSettingsFields"></div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button id="serverSettingsSaveBtn"><i class="fa fa-save"></i> Speichern</button>
|
||||||
|
</div>
|
||||||
|
<div id="serverSettingsMessage" class="profile-message" hidden></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template id="tpl-admin">
|
<template id="tpl-admin">
|
||||||
<div class="page form-page">
|
<div class="page form-page">
|
||||||
<header>
|
<header>
|
||||||
@@ -536,6 +556,7 @@
|
|||||||
<script src="js/pages/settings-page.js"></script>
|
<script src="js/pages/settings-page.js"></script>
|
||||||
<script src="js/pages/profile-page.js"></script>
|
<script src="js/pages/profile-page.js"></script>
|
||||||
<script src="js/pages/admin-page.js"></script>
|
<script src="js/pages/admin-page.js"></script>
|
||||||
|
<script src="js/pages/server-settings-page.js"></script>
|
||||||
<script src="js/pages/shared-page.js"></script>
|
<script src="js/pages/shared-page.js"></script>
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -490,6 +490,27 @@ async function apiAdminGetLoginLink(userId) {
|
|||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Server Settings API (Admin) ──
|
||||||
|
|
||||||
|
async function apiGetServerSettings() {
|
||||||
|
const resp = await fetch('/api/admin/server-settings');
|
||||||
|
if (!resp.ok) throw new Error('Fehler beim Laden der Servereinstellungen');
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiSaveServerSettings(data) {
|
||||||
|
const resp = await fetch('/api/admin/server-settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Krea Image API ──
|
// ── Krea Image API ──
|
||||||
|
|
||||||
async function apiKreaStatus() {
|
async function apiKreaStatus() {
|
||||||
|
|||||||
@@ -14,14 +14,20 @@ registerPage('dashboard', {
|
|||||||
navigateTo('profile');
|
navigateTo('profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin-Link (nur für Admins sichtbar)
|
// Admin-Links (nur für Admins sichtbar)
|
||||||
var adminLink = document.getElementById('dashboardAdminLink');
|
var adminLink = document.getElementById('dashboardAdminLink');
|
||||||
|
var serverSettingsLink = document.getElementById('dashboardServerSettingsLink');
|
||||||
if (currentUser && currentUser.isAdmin) {
|
if (currentUser && currentUser.isAdmin) {
|
||||||
adminLink.hidden = false;
|
adminLink.hidden = false;
|
||||||
adminLink.addEventListener('click', (e) => {
|
adminLink.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigateTo('admin');
|
navigateTo('admin');
|
||||||
});
|
});
|
||||||
|
serverSettingsLink.hidden = false;
|
||||||
|
serverSettingsLink.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateTo('server-settings');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globaler Click-Handler zum Schließen aller Card-Menüs
|
// Globaler Click-Handler zum Schließen aller Card-Menüs
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// ── Server Settings Page (Admin) ──
|
||||||
|
|
||||||
|
registerPage('server-settings', {
|
||||||
|
async init() {
|
||||||
|
if (!currentUser || !currentUser.isAdmin) {
|
||||||
|
navigateTo('dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = document.getElementById('serverSettingsFields');
|
||||||
|
var saveBtn = document.getElementById('serverSettingsSaveBtn');
|
||||||
|
var messageEl = document.getElementById('serverSettingsMessage');
|
||||||
|
var inputs = {};
|
||||||
|
|
||||||
|
function showMessage(text, isError) {
|
||||||
|
messageEl.textContent = text;
|
||||||
|
messageEl.className = 'profile-message ' + (isError ? 'profile-message-error' : 'profile-message-success');
|
||||||
|
messageEl.hidden = false;
|
||||||
|
if (!isError) setTimeout(function() { messageEl.hidden = true; }, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings laden und Formular aufbauen
|
||||||
|
try {
|
||||||
|
var result = await apiGetServerSettings();
|
||||||
|
var settings = result.settings;
|
||||||
|
|
||||||
|
for (var i = 0; i < settings.length; i++) {
|
||||||
|
var s = settings[i];
|
||||||
|
var field = document.createElement('div');
|
||||||
|
field.className = 'form-field';
|
||||||
|
|
||||||
|
var label = document.createElement('label');
|
||||||
|
label.textContent = s.label;
|
||||||
|
label.setAttribute('for', 'ss-' + s.key);
|
||||||
|
field.appendChild(label);
|
||||||
|
|
||||||
|
if (s.inputType === 'toggle') {
|
||||||
|
var toggleLabel = document.createElement('label');
|
||||||
|
toggleLabel.className = 'toggle-label';
|
||||||
|
|
||||||
|
var cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.id = 'ss-' + s.key;
|
||||||
|
cb.checked = !!s.value;
|
||||||
|
inputs[s.key] = cb;
|
||||||
|
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.textContent = s.value ? 'Aktiviert' : 'Deaktiviert';
|
||||||
|
|
||||||
|
cb.addEventListener('change', (function(sp, checkbox) {
|
||||||
|
return function() { sp.textContent = checkbox.checked ? 'Aktiviert' : 'Deaktiviert'; };
|
||||||
|
})(span, cb));
|
||||||
|
|
||||||
|
toggleLabel.appendChild(cb);
|
||||||
|
toggleLabel.appendChild(span);
|
||||||
|
field.appendChild(toggleLabel);
|
||||||
|
} else if (s.inputType === 'password') {
|
||||||
|
var wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'input-with-btn';
|
||||||
|
|
||||||
|
var inp = document.createElement('input');
|
||||||
|
inp.type = 'password';
|
||||||
|
inp.id = 'ss-' + s.key;
|
||||||
|
inp.value = s.value || '';
|
||||||
|
inp.placeholder = s.label;
|
||||||
|
inp.autocomplete = 'off';
|
||||||
|
inputs[s.key] = inp;
|
||||||
|
|
||||||
|
var toggleBtn = document.createElement('button');
|
||||||
|
toggleBtn.type = 'button';
|
||||||
|
toggleBtn.className = 'icon-btn';
|
||||||
|
toggleBtn.innerHTML = '<i class="fa fa-eye"></i>';
|
||||||
|
toggleBtn.addEventListener('click', (function(input, btn) {
|
||||||
|
return function() {
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
btn.innerHTML = '<i class="fa fa-eye-slash"></i>';
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
btn.innerHTML = '<i class="fa fa-eye"></i>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(inp, toggleBtn));
|
||||||
|
|
||||||
|
wrapper.appendChild(inp);
|
||||||
|
wrapper.appendChild(toggleBtn);
|
||||||
|
field.appendChild(wrapper);
|
||||||
|
} else if (s.inputType === 'number') {
|
||||||
|
var inp = document.createElement('input');
|
||||||
|
inp.type = 'number';
|
||||||
|
inp.id = 'ss-' + s.key;
|
||||||
|
inp.value = s.value != null ? s.value : '';
|
||||||
|
inp.placeholder = s.label;
|
||||||
|
inputs[s.key] = inp;
|
||||||
|
field.appendChild(inp);
|
||||||
|
} else {
|
||||||
|
var inp = document.createElement('input');
|
||||||
|
inp.type = 'text';
|
||||||
|
inp.id = 'ss-' + s.key;
|
||||||
|
inp.value = s.value || '';
|
||||||
|
inp.placeholder = s.label;
|
||||||
|
inputs[s.key] = inp;
|
||||||
|
field.appendChild(inp);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichern
|
||||||
|
saveBtn.addEventListener('click', async function() {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
messageEl.hidden = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var data = {};
|
||||||
|
for (var j = 0; j < settings.length; j++) {
|
||||||
|
var def = settings[j];
|
||||||
|
var input = inputs[def.key];
|
||||||
|
if (!input) continue;
|
||||||
|
|
||||||
|
if (def.type === 'bool') {
|
||||||
|
data[def.key] = input.checked;
|
||||||
|
} else if (def.type === 'int') {
|
||||||
|
data[def.key] = parseInt(input.value, 10) || 0;
|
||||||
|
} else {
|
||||||
|
data[def.key] = input.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await apiSaveServerSettings(data);
|
||||||
|
showMessage(result.hint || 'Einstellungen gespeichert', false);
|
||||||
|
} catch (err) {
|
||||||
|
showMessage(err.message, true);
|
||||||
|
} finally {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
container.innerHTML = '<p class="form-error">Fehler beim Laden: ' + esc(err.message) + '</p>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user