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 @@ + +