using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Data.Sqlite; namespace Gott.Services; /// /// Zentraler Settings-Service. Liest/schreibt aus der DB-Tabelle 'server_settings'. /// In-Memory-Cache für schnellen Zugriff. Kein Neustart bei Änderungen nötig. /// public class ServerSettingsService { private readonly string _dbPath; private readonly Dictionary _cache = new Dictionary(); private readonly object _lock = new object(); // Definiert alle Settings mit Typ, Default und Beschreibung public static readonly List Definitions = new List { new SettingDefinition( "ProxyUrl", "string", "", "HTTP/SOCKS Proxy (erfordert Neustart)", "text" ), new SettingDefinition( "MaxIterations", "int", "100", "Max Tool-Loop Iterationen pro Chat", "number" ), new SettingDefinition( "MaxHistory", "int", "220", "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( "GeminiApiKey", "string", "", "Gemini API-Key (für Gemini-Provider im Chat)", "password" ), new SettingDefinition( "GitHubToken", "string", "", "GitHub PAT für Key-Scanner und Code-Search Tool", "password" ), new SettingDefinition( "SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner + Tool)", "password" ), new SettingDefinition( "ScanDailyHour", "int", "-1", "Key-Scanner Full-Scan Uhrzeit (0-23, -1 = deaktiviert)", "number" ), new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ), new SettingDefinition( "HuggingFaceToken", "string", "", "HuggingFace Token (optional, erhöht Rate-Limits)", "password" ), new SettingDefinition( "NvidiaApiKey", "string", "", "Nvidia API-Key (direkt statt g4f.space Proxy)", "password" ), new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ), }; public ServerSettingsService(string dbPath) { _dbPath = dbPath; EnsureTable(); LoadCache(); } private void EnsureTable() { using var conn = new SqliteConnection($"Data Source={_dbPath}"); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = @" CREATE TABLE IF NOT EXISTS server_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '', type TEXT NOT NULL DEFAULT 'string' )"; cmd.ExecuteNonQuery(); } private void LoadCache() { lock (_lock) { _cache.Clear(); using var conn = new SqliteConnection($"Data Source={_dbPath}"); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT key, value FROM server_settings"; using var reader = cmd.ExecuteReader(); while (reader.Read()) { _cache[reader.GetString(0)] = reader.GetString(1); } } } /// /// Entfernt veraltete Settings aus der DB die nicht mehr in Definitions existieren. /// private void CleanupObsoleteSettings() { var obsoleteKeys = new[] { "ScanIntervalHours" }; using var conn = new SqliteConnection($"Data Source={_dbPath}"); conn.Open(); foreach (var key in obsoleteKeys) { using var cmd = conn.CreateCommand(); cmd.CommandText = "DELETE FROM server_settings WHERE key = @key"; cmd.Parameters.AddWithValue("@key", key); cmd.ExecuteNonQuery(); } lock (_lock) { foreach (var key in obsoleteKeys) _cache.Remove(key); } } /// /// Migriert bestehende Werte aus appsettings.Production.json in die DB (einmalig). /// Nur Settings die noch NICHT in der DB sind werden übernommen. /// public void MigrateFromAppSettings(string contentRootPath) { CleanupObsoleteSettings(); var prodPath = Path.Combine(contentRootPath, "appsettings.Production.json"); if (!File.Exists(prodPath)) return; try { var json = File.ReadAllText(prodPath); var obj = JsonNode.Parse(json) as JsonObject; if (obj == null) return; // Port bleibt in appsettings – nicht migrieren var skipKeys = new HashSet { "Port", "Logging", "AllowedHosts" }; foreach (var def in Definitions) { if (skipKeys.Contains(def.Key)) continue; if (!obj.ContainsKey(def.Key)) continue; // Nur migrieren wenn nicht schon in DB lock (_lock) { if (_cache.ContainsKey(def.Key)) continue; } var node = obj[def.Key]; string value; switch (def.Type) { case "bool": value = (node?.GetValue() ?? false) ? "true" : "false"; break; case "int": value = (node?.GetValue() ?? 0).ToString(); break; default: value = node?.GetValue() ?? ""; break; } // Default-Werte nicht migrieren (kein Müll in DB) if (value == def.DefaultValue) continue; Set(def.Key, value); } } catch { // Fehler bei Migration ignorieren – Settings sind halt Default } } // ─── Getter ──────────────────────────────────────────────── public string Get(string key, string defaultValue = "") { lock (_lock) { return _cache.TryGetValue(key, out var val) ? val : defaultValue; } } public int GetInt(string key, int defaultValue = 0) { var raw = Get(key, null); return raw != null && int.TryParse(raw, out var v) ? v : defaultValue; } public bool GetBool(string key, bool defaultValue = false) { var raw = Get(key, null); if (raw == null) return defaultValue; return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw == "1"; } // ─── Setter ──────────────────────────────────────────────── public void Set(string key, string value) { // Typ aus Definitions holen var def = Definitions.Find(d => d.Key == key); var type = def?.Type ?? "string"; using var conn = new SqliteConnection($"Data Source={_dbPath}"); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = @" INSERT INTO server_settings (key, value, type) VALUES (@key, @value, @type) ON CONFLICT(key) DO UPDATE SET value = @value, type = @type"; cmd.Parameters.AddWithValue("@key", key); cmd.Parameters.AddWithValue("@value", value ?? ""); cmd.Parameters.AddWithValue("@type", type); cmd.ExecuteNonQuery(); lock (_lock) { _cache[key] = value ?? ""; } } public void SetInt(string key, int value) => Set(key, value.ToString()); public void SetBool(string key, bool value) => Set(key, value ? "true" : "false"); public string[] GetStringArray(string key, string defaultCsv = "") { var raw = Get(key, null); if (raw == null) { var def = Definitions.Find(d => d.Key == key); raw = def?.DefaultValue ?? defaultCsv; } if (string.IsNullOrWhiteSpace(raw)) return Array.Empty(); return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } public void SetStringArray(string key, string[] values) { Set(key, string.Join(",", values ?? Array.Empty())); } // ─── Bulk-Operationen (für Endpoint) ─────────────────────── public List GetAll() { var result = new List(); foreach (var def in Definitions) { object value; switch (def.Type) { case "int": value = GetInt(def.Key, int.TryParse(def.DefaultValue, out var dv) ? dv : 0); break; case "bool": value = GetBool(def.Key, def.DefaultValue.Equals("true", StringComparison.OrdinalIgnoreCase)); break; case "string_array": value = GetStringArray(def.Key, def.DefaultValue); break; default: value = Get(def.Key, def.DefaultValue); break; } // Zusatz-Metadaten für bestimmte Settings object options = null; if (def.Key == "ScannerKeyTypes") { options = KeyPatterns.AllTypes.Select(t => new { id = t, name = KeyPatterns.DisplayName(t) }).ToArray(); } result.Add(new { key = def.Key, type = def.Type, value = value, label = def.Label, inputType = def.InputType, options = options, }); } return result; } // ─── Definition ──────────────────────────────────────────── public class SettingDefinition { public string Key { get; } public string Type { get; } public string DefaultValue { get; } public string Label { get; } public string InputType { get; } public SettingDefinition(string key, string type, string defaultValue, string label, string inputType) { Key = key; Type = type; DefaultValue = defaultValue; Label = label; InputType = inputType; } } }