Problem: systemd setzt WorkingDirectory=/app/gott (= ContentRoot), aber AppContext.BaseDirectory zeigt auf /app/gott/publish/. Dadurch existierten zwei appsettings.Production.json – .NET las die eine, die Server-Settings-UI schrieb die andere. Config-Änderungen wie PayloadLogging hatten daher keinen Effekt. Fix: ProductionConfigPath und FallbackConfigPath werden jetzt von app.Environment.ContentRootPath abgeleitet statt AppContext.BaseDirectory.
182 lines
6.4 KiB
C#
182 lines
6.4 KiB
C#
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
|
||
{
|
||
// Pfade werden beim MapEndpoints gesetzt – basierend auf ContentRootPath,
|
||
// da .NET die Config-Dateien vom ContentRoot lädt (= WorkingDirectory),
|
||
// nicht von AppContext.BaseDirectory (= Executable-Verzeichnis).
|
||
private static string ProductionConfigPath;
|
||
private static string FallbackConfigPath;
|
||
|
||
// Definiert alle konfigurierbaren Properties mit Typ und Standardwert
|
||
private static readonly List<SettingDefinition> Definitions = new List<SettingDefinition>
|
||
{
|
||
new SettingDefinition("Port", "int", 19000, "Server-Port (erfordert Neustart)", "number"),
|
||
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)
|
||
{
|
||
var contentRoot = app.Environment.ContentRootPath;
|
||
ProductionConfigPath = Path.Combine(contentRoot, "appsettings.Production.json");
|
||
FallbackConfigPath = Path.Combine(contentRoot, "appsettings.json");
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|