From bfd78278ab88534531acd38fd5d2fab6a1348359 Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 6 Mar 2026 22:32:33 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Servereinstellungen-Seite=20f=C3=BCr=20?= =?UTF-8?q?Admins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- Endpoints/ServerSettingsEndpoint.cs | 176 +++++++++++++++++++++++ Program.cs | 1 + gott.csproj | 4 + wwwroot/css/form.css | 38 ++++- wwwroot/index.html | 21 +++ wwwroot/js/api.js | 21 +++ wwwroot/js/pages/dashboard.js | 8 +- wwwroot/js/pages/server-settings-page.js | 145 +++++++++++++++++++ 8 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 Endpoints/ServerSettingsEndpoint.cs create mode 100644 wwwroot/js/pages/server-settings-page.js diff --git a/Endpoints/ServerSettingsEndpoint.cs b/Endpoints/ServerSettingsEndpoint.cs new file mode 100644 index 0000000..c805276 --- /dev/null +++ b/Endpoints/ServerSettingsEndpoint.cs @@ -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 Definitions = new List + { + 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(); + + 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(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(); + case "int": return node.GetValue(); + case "bool": return node.GetValue(); + 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; + } + } +} diff --git a/Program.cs b/Program.cs index 8f1f2a4..f1114d1 100644 --- a/Program.cs +++ b/Program.cs @@ -82,5 +82,6 @@ app.MapCronjobEndpoints(); app.MapKreaEndpoints(); app.MapSharedEndpoints(); app.MapAdminEndpoints(); +app.MapServerSettingsEndpoints(); app.Run("http://0.0.0.0:19000"); diff --git a/gott.csproj b/gott.csproj index 6fe1bb5..579094c 100755 --- a/gott.csproj +++ b/gott.csproj @@ -30,6 +30,10 @@ + + + + diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index a19d99a..152470a 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -566,11 +566,45 @@ label.skill-item { .form-hint { display: block; - margin-top: 4px; - font-size: var(--font-size); 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 { diff --git a/wwwroot/index.html b/wwwroot/index.html index 4ad793e..b1fd379 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -57,6 +57,7 @@ @@ -444,6 +445,25 @@ + +