Refactor: Server-Settings von appsettings.json in DB-Tabelle (server_settings)

- Neuer ServerSettingsService: zentrale Settings in SQLite mit In-Memory-Cache
- Auto-Migration: bestehende appsettings.Production.json wird einmalig in DB übernommen
- Alle Services umgestellt: IConfiguration → ServerSettingsService (dynamisch, kein Neustart)
- Port bleibt als einziges Setting in appsettings.Production.json (erfordert Neustart)
- ServerSettingsEndpoint: liest/schreibt DB statt JSON-Datei, Port als Sonder-Setting
- IConfiguration komplett entfernt aus allen Services (ChatService, ToolService, AnthropicService,
  OpenAiService, KreaService, TelegramService, KeyScannerService, GitHubEventScannerService)
- AuthMiddleware: KreaApiKey dynamisch aus DB
- appsettings.json bereinigt: nur noch Port und Logging
- Dokumentation aktualisiert (ARCHITECTURE.md, ARCHITECTURE_QUICK.md, README.md)
This commit is contained in:
Meleki committed 2026-03-14 13:06:44 +01:00
1 parent 542a031172
commit 122000487c
18 files changed
+389 -270

No files matched your search

+2 -3
View File
@@ -10,7 +10,6 @@ using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
namespace Gott.Endpoints;
@@ -22,7 +21,7 @@ public static class ChatEndpoint
app.MapPost("/api/chat/stop", HandleStop);
}
private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, OpenAiService openAi, RemoteConnectionManager remoteMgr, EmbeddingBackgroundService embeddingBg, CancellationToken cancellationToken)
private static async Task HandleChat(HttpRequest req, HttpResponse res, ServerSettingsService settings, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, OpenAiService openAi, RemoteConnectionManager remoteMgr, EmbeddingBackgroundService embeddingBg, CancellationToken cancellationToken)
{
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
if (body == null)
@@ -61,7 +60,7 @@ public static class ChatEndpoint
return;
}
var maxIterations = config.GetValue<int>("MaxIterations", 100);
var maxIterations = settings.GetInt("MaxIterations", 100);
var hasRemote = remoteMgr.GetBySessionId(sessionId) != null;
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote);
+59 -123
View File
@@ -5,6 +5,7 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Gott.Extensions;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
@@ -13,42 +14,13 @@ 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("MaxIterations", "int", 100, "Max Tool-Loop Iterationen pro Chat", "number"),
new SettingDefinition("MaxHistory", "int", 120, "Max History-Messages pro Request", "number"),
new SettingDefinition("ReminderInterval", "int", 30, "System-Prompt Reminder alle N Messages (0 = deaktiviert)", "number"),
new SettingDefinition("ExecuteTimeout", "int", 180, "Timeout für Shell-Befehle in Sekunden", "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"),
new SettingDefinition("GitHubToken", "string", "", "GitHub PAT für Key-Scanner", "password"),
new SettingDefinition("SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner)", "password"),
new SettingDefinition("ProxyUrl", "string", "", "HTTP/SOCKS Proxy für alle Requests (z.B. socks5://127.0.0.1:1080)", "text"),
new SettingDefinition("ScanIntervalHours", "int", 0, "Key-Scanner Intervall in Stunden (0 = nur manuell)", "number"),
new SettingDefinition("GitHubEventScanner", "bool", false, "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle"),
};
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)
private static async Task HandleGet(HttpContext ctx, ServerSettingsService settings, IConfiguration config)
{
if (!ctx.IsAdmin())
{
@@ -57,54 +29,25 @@ public static class ServerSettingsEndpoint
return;
}
// Production-Config lesen (falls vorhanden)
JsonObject prodConfig = null;
if (File.Exists(ProductionConfigPath))
// Port aus appsettings (nicht DB) – als erstes Feld
var port = config.GetValue<int>("Port", 19000);
var allSettings = new List<object>
{
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))
new
{
value = ExtractValue(fallbackConfig[def.Key], def.Type);
key = "Port",
type = "int",
value = (object)port,
label = "Server-Port (erfordert Neustart)",
inputType = "number",
}
};
allSettings.AddRange(settings.GetAll());
// 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 });
await ctx.Response.WriteAsJsonAsync(new { settings = allSettings });
}
private static async Task HandleUpdate(HttpContext ctx)
private static async Task HandleUpdate(HttpContext ctx, ServerSettingsService settings, IConfiguration config)
{
if (!ctx.IsAdmin())
{
@@ -114,75 +57,68 @@ public static class ServerSettingsEndpoint
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var hint = "";
// Bestehende Production-Config laden oder neue erstellen
JsonObject prodConfig;
if (File.Exists(ProductionConfigPath))
// Port separat behandeln – in appsettings.Production.json speichern
if (body.TryGetProperty("Port", out var portProp) && portProp.TryGetInt32(out var newPort))
{
var existing = await File.ReadAllTextAsync(ProductionConfigPath);
prodConfig = JsonNode.Parse(existing) as JsonObject ?? new JsonObject();
}
else
{
prodConfig = new JsonObject();
var currentPort = config.GetValue<int>("Port", 19000);
if (newPort != currentPort && newPort > 0 && newPort <= 65535)
{
SavePortToAppSettings(ctx, newPort);
hint = "Port geändert – Neustart erforderlich!";
}
}
// Nur definierte Keys akzeptieren
foreach (var def in Definitions)
// Alle anderen Settings in DB speichern
foreach (var def in ServerSettingsService.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() ?? "");
settings.Set(def.Key, prop.GetString() ?? "");
break;
case "int":
prodConfig.Add(def.Key, prop.TryGetInt32(out var i) ? i : (int)def.DefaultValue);
settings.SetInt(def.Key, prop.TryGetInt32(out var i) ? i : 0);
break;
case "bool":
prodConfig.Add(def.Key, prop.ValueKind == JsonValueKind.True);
settings.SetBool(def.Key, prop.ValueKind == JsonValueKind.True);
break;
}
}
await ctx.Response.WriteAsJsonAsync(new { ok = true, hint = string.IsNullOrEmpty(hint) ? "Einstellungen gespeichert" : hint });
}
private static void SavePortToAppSettings(HttpContext ctx, int port)
{
// appsettings.Production.json im ContentRootPath (= publish-Verzeichnis)
var contentRoot = ctx.RequestServices.GetService(typeof(Microsoft.AspNetCore.Hosting.IWebHostEnvironment)) as Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
var path = Path.Combine(contentRoot?.ContentRootPath ?? AppContext.BaseDirectory, "appsettings.Production.json");
JsonObject obj;
if (File.Exists(path))
{
try
{
var existing = File.ReadAllText(path);
obj = JsonNode.Parse(existing) as JsonObject ?? new JsonObject();
}
catch
{
obj = new JsonObject();
}
}
else
{
obj = new JsonObject();
}
obj["Port"] = port;
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;
}
File.WriteAllText(path, obj.ToJsonString(options));
}
}