Settings nach Avalonia - PropertyGrid durch kategorisierten Editor ersetzt
Der PropertyGrid-Ersatz ist EIN wiederverwendbares Steuerelement statt Handarbeit je Feld: Controls/SettingsEditor zeigt ein beliebiges Einstellungsobjekt nach Richards Vorlage - Kategorie-Ueberschrift, Beschriftung links, Feld rechts, Erklaerung klein darunter. - SettingsModelBuilder liest die Attribute, die fuers PropertyGrid ohnehin gepflegt waren: [Category] gruppiert, [DisplayName] beschriftet, [Description] wird zum Hinweistext, [Browsable(false)] blendet aus (Watchdog-Token, Lizenzschluessel bleiben unsichtbar). Ergebnis fuer ServerSettings: 6 Abschnitte, 15 Felder - ohne eine Zeile Feld-Code. - Layout-Regel gewahrt: WELCHE Felder es gibt, kommt als Daten; WIE ein Feld aussieht, steht deklarativ als DataTemplate je Feldtyp (Text/Zahl/Ja-Nein/Auswahl/Nur-Lese). - Damit sind auch die drei restlichen PropertyGrid-Stellen (Account-Einstellungen, Master-Trader, ResolutionFarming) mit je einem Aufruf erledigt. SettingsWindow: Server-Settings + Polymarket-Accounts, Master-Key erzeugen, OpenRouter-Key/Watchdog-Token setzen, Test-Heartbeat. Rueckmeldungen laufen ueber die Statuszeile statt ueber Dialoge - nur echte Entscheidungen bekommen einen Dialog (neu: Views/DialogWindow fuer Hinweis/Rueckfrage/maskierte Eingabe, Avalonia hat keine MessageBox). Nebenbei einen offenen Punkt der Linux-Analyse erledigt: Schluesseldateien (master.key, openrouter.key) werden jetzt per File.SetUnixFileMode auf 600 gesetzt. Auf Linux legt File.WriteAllText sonst mit ueblicher umask 644 an - weltweit lesbar. Lizenzdialog bewusst NICHT portiert: haengt am alten LicenseLabrador-SDK, das mit der Deploymentcenter-Anbindung (P3c) ohnehin ersetzt wird - waere Wegwerfarbeit. Smoke-UI prueft jetzt zusaetzlich die Feldzahl des Editors: ein Fenster kann fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen. Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (5 Fenster + Editor-Pruefung), Linux-Publish laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="PolyTrader.App.Avalonia.Views.DialogWindow"
|
||||
Width="520" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<!--
|
||||
Ein Dialog für alle drei Fälle, die die WinForms-Oberfläche über MessageBox/Prompt löste:
|
||||
Hinweis, Ja/Nein-Rückfrage und (maskierte) Eingabe. Avalonia bringt keine MessageBox mit;
|
||||
ein eigener Dialog ist der ehrlichere Weg als eine Fremdbibliothek für drei Anwendungsfälle.
|
||||
-->
|
||||
<StackPanel Margin="20" Spacing="12">
|
||||
<TextBlock Name="lblTitle" FontSize="15" FontWeight="SemiBold" />
|
||||
<TextBlock Name="lblMessage" TextWrapping="Wrap" />
|
||||
|
||||
<TextBox Name="txtInput" IsVisible="False" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Name="btnCancel" Content="Abbrechen" Padding="16,6" IsCancel="True" />
|
||||
<Button Name="btnOk" Content="OK" Padding="16,6" IsDefault="True" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace PolyTrader.App.Avalonia.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Kleiner Allzweck-Dialog: Hinweis, Ja/Nein-Rückfrage und (maskierte) Eingabe.
|
||||
/// Ersetzt die MessageBox-/Prompt-Aufrufe der WinForms-Oberfläche.
|
||||
/// </summary>
|
||||
public partial class DialogWindow : Window
|
||||
{
|
||||
public DialogWindow() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
private void Setup(string title, string message, bool withInput, bool masked, string okText, bool withCancel)
|
||||
{
|
||||
Title = title;
|
||||
this.FindControl<TextBlock>("lblTitle")!.Text = title;
|
||||
this.FindControl<TextBlock>("lblMessage")!.Text = message;
|
||||
|
||||
var input = this.FindControl<TextBox>("txtInput")!;
|
||||
input.IsVisible = withInput;
|
||||
if (withInput)
|
||||
{
|
||||
input.PasswordChar = masked ? '•' : '\0';
|
||||
input.Text = string.Empty;
|
||||
}
|
||||
|
||||
var ok = this.FindControl<Button>("btnOk")!;
|
||||
var cancel = this.FindControl<Button>("btnCancel")!;
|
||||
ok.Content = okText;
|
||||
cancel.IsVisible = withCancel;
|
||||
|
||||
ok.Click += (_, _) => Close(true);
|
||||
cancel.Click += (_, _) => Close(false);
|
||||
}
|
||||
|
||||
/// <summary>Reiner Hinweis mit OK.</summary>
|
||||
public static async Task Info(Window owner, string title, string message)
|
||||
{
|
||||
var dlg = new DialogWindow();
|
||||
dlg.Setup(title, message, withInput: false, masked: false, okText: "OK", withCancel: false);
|
||||
await dlg.ShowDialog(owner);
|
||||
}
|
||||
|
||||
/// <summary>Ja/Nein-Rückfrage. Liefert <c>true</c> bei Bestätigung.</summary>
|
||||
public static async Task<bool> Confirm(Window owner, string title, string message, string okText = "Ja")
|
||||
{
|
||||
var dlg = new DialogWindow();
|
||||
dlg.Setup(title, message, withInput: false, masked: false, okText: okText, withCancel: true);
|
||||
return await dlg.ShowDialog<bool>(owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eingabe. Liefert <c>null</c> bei Abbruch – leerer String bedeutet bewusst „Wert entfernen"
|
||||
/// (dieselbe Unterscheidung wie in der bisherigen Oberfläche).
|
||||
/// </summary>
|
||||
public static async Task<string?> Prompt(Window owner, string title, string message, bool masked = true)
|
||||
{
|
||||
var dlg = new DialogWindow();
|
||||
dlg.Setup(title, message, withInput: true, masked: masked, okText: "OK", withCancel: true);
|
||||
bool ok = await dlg.ShowDialog<bool>(owner);
|
||||
return ok ? dlg.FindControl<TextBox>("txtInput")!.Text ?? string.Empty : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:PolyTrader.App.Avalonia.Controls"
|
||||
xmlns:models="using:PolyTraderSharp.Models"
|
||||
x:Class="PolyTrader.App.Avalonia.Views.SettingsWindow"
|
||||
Title="Server Settings"
|
||||
Width="1185" Height="820">
|
||||
|
||||
<!--
|
||||
Einstellungen. Ersetzt die beiden PropertyGrids der WinForms-Fassung durch den
|
||||
kategorisierten SettingsEditor (Überschrift, Beschriftung links, Feld rechts,
|
||||
Erklärung klein darunter).
|
||||
-->
|
||||
<DockPanel>
|
||||
<controls:WindowMenuBar Name="menuBar" DockPanel.Dock="Top" />
|
||||
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<TextBlock Name="lblStatus" Text="Bereit." />
|
||||
</Border>
|
||||
|
||||
<TabControl>
|
||||
|
||||
<TabItem Header="Allgemeine Einstellungen">
|
||||
<DockPanel>
|
||||
<Border Classes="toolbar" DockPanel.Dock="Top">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Name="btnSave" Content="Speichern" />
|
||||
<Button Name="btnReload" Content="Neu laden" />
|
||||
<Separator Margin="6,0" />
|
||||
<Button Name="btnGenMasterKey" Content="Master-Key erzeugen"
|
||||
ToolTip.Tip="Erzeugt einen zufälligen AES-Master-Key (nur wenn noch keiner existiert)." />
|
||||
<Button Name="btnOpenRouterKey" Content="OpenRouter-Key setzen …"
|
||||
ToolTip.Tip="Speichert den OpenRouter-API-Key für den Supervisor (gitignorierte Datei openrouter.key)." />
|
||||
<Button Name="btnWatchdogToken" Content="Watchdog-Token setzen …"
|
||||
ToolTip.Tip="Speichert den Watchdog-Agent-Token (maskierte Eingabe, bei gesetztem Master-Key verschlüsselt)." />
|
||||
<Button Name="btnTestWatchdog" Content="Test-Heartbeat senden"
|
||||
ToolTip.Tip="Sendet sofort einen Heartbeat an den konfigurierten Watchdog-Server." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<controls:SettingsEditor Name="editorServer" />
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Polymarket Accounts">
|
||||
<DockPanel>
|
||||
<Border Classes="toolbar" DockPanel.Dock="Top">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Name="btnAccNew" Content="Neuer Account" />
|
||||
<Button Name="btnAccDelete" Content="Löschen" />
|
||||
<Button Name="btnAccSave" Content="Account speichern" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid ColumnDefinitions="380,4,*">
|
||||
<DataGrid Name="gridAccounts" Grid.Column="0" x:DataType="models:AccountState">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="#" Width="50" Binding="{Binding AccountId}" />
|
||||
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Name}" />
|
||||
<DataGridCheckBoxColumn Header="Demo" Width="60" Binding="{Binding IsDemo}" />
|
||||
<DataGridCheckBoxColumn Header="Aktiv" Width="60" Binding="{Binding IsActive}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<GridSplitter Grid.Column="1" Background="#DDDDDD" />
|
||||
|
||||
<controls:SettingsEditor Name="editorAccount" Grid.Column="2" />
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,327 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using PolyTrader.App.Avalonia.Controls;
|
||||
using PolyTrader.Core.Modularity;
|
||||
using PolyTrader.Core.Persistence;
|
||||
using PolyTrader.Core.Security;
|
||||
using PolyTraderSharp;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.App.Avalonia.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Einstellungen: allgemeine Server-Settings und die Polymarket-Accounts. Beide Bereiche nutzen
|
||||
/// den <see cref="SettingsEditor"/> anstelle der früheren PropertyGrids.
|
||||
/// Layout vollständig in SettingsWindow.axaml.
|
||||
/// </summary>
|
||||
public partial class SettingsWindow : Window
|
||||
{
|
||||
private const string SettingsPath = "server_settings.xml";
|
||||
|
||||
private readonly ObservableCollection<AccountState> _accounts = new();
|
||||
|
||||
private ServerSettings _settings = new();
|
||||
private IAccountRepository? _accountRepo;
|
||||
private TradingState? _state;
|
||||
private WatchdogHeartbeatService? _watchdog;
|
||||
private MullvadVpnService? _vpn;
|
||||
private TerminalLogger? _logger;
|
||||
|
||||
public SettingsWindow() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
public SettingsWindow(IModuleUiHost host, IAccountRepository accountRepo, TradingState state,
|
||||
TerminalLogger logger, WatchdogHeartbeatService? watchdog = null,
|
||||
MullvadVpnService? vpn = null) : this()
|
||||
{
|
||||
this.FindControl<Controls.WindowMenuBar>("menuBar")!.Attach(host, "core.settings", this);
|
||||
|
||||
_accountRepo = accountRepo;
|
||||
_state = state;
|
||||
_logger = logger;
|
||||
_watchdog = watchdog;
|
||||
_vpn = vpn;
|
||||
|
||||
var grid = this.FindControl<DataGrid>("gridAccounts")!;
|
||||
grid.ItemsSource = _accounts;
|
||||
grid.SelectionChanged += (_, _) =>
|
||||
this.FindControl<SettingsEditor>("editorAccount")!.Show(grid.SelectedItem);
|
||||
|
||||
this.FindControl<Button>("btnSave")!.Click += (_, _) => SaveServerSettings();
|
||||
this.FindControl<Button>("btnReload")!.Click += (_, _) => ReloadServerSettings();
|
||||
this.FindControl<Button>("btnGenMasterKey")!.Click += async (_, _) => await GenerateMasterKeyAsync();
|
||||
this.FindControl<Button>("btnOpenRouterKey")!.Click += async (_, _) => await SetOpenRouterKeyAsync();
|
||||
this.FindControl<Button>("btnWatchdogToken")!.Click += async (_, _) => await SetWatchdogTokenAsync();
|
||||
this.FindControl<Button>("btnTestWatchdog")!.Click += async (_, _) => await TestWatchdogAsync();
|
||||
|
||||
this.FindControl<Button>("btnAccNew")!.Click += (_, _) => AddAccount();
|
||||
this.FindControl<Button>("btnAccDelete")!.Click += async (_, _) => await DeleteAccountAsync();
|
||||
this.FindControl<Button>("btnAccSave")!.Click += (_, _) => SaveSelectedAccount();
|
||||
|
||||
ReloadServerSettings();
|
||||
LoadAccounts();
|
||||
UpdateMasterKeyButtonState();
|
||||
}
|
||||
|
||||
private void Status(string text) => this.FindControl<TextBlock>("lblStatus")!.Text = text;
|
||||
|
||||
// ===== Server-Settings =====
|
||||
|
||||
private void ReloadServerSettings()
|
||||
{
|
||||
_settings = ServerSettings.Load(SettingsPath);
|
||||
this.FindControl<SettingsEditor>("editorServer")!.Show(_settings);
|
||||
Status("Einstellungen geladen.");
|
||||
}
|
||||
|
||||
private void SaveServerSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
_settings.Save(SettingsPath);
|
||||
_vpn?.ReloadSettings();
|
||||
_watchdog?.ReloadSettings();
|
||||
_logger?.Info("Server-Einstellungen gespeichert und Dienste neu geladen.");
|
||||
Status($"Gespeichert um {DateTime.Now:HH:mm:ss}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status($"Speichern fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Polymarket Accounts =====
|
||||
|
||||
private void LoadAccounts()
|
||||
{
|
||||
if (_state == null) return;
|
||||
_accounts.Clear();
|
||||
foreach (var a in _state.Accounts.Values.OrderBy(a => a.AccountId)) _accounts.Add(a);
|
||||
|
||||
var grid = this.FindControl<DataGrid>("gridAccounts")!;
|
||||
if (_accounts.Count > 0) grid.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void AddAccount()
|
||||
{
|
||||
if (_state == null || _accountRepo == null) return;
|
||||
|
||||
int newId = _state.Accounts.Count > 0 ? _state.Accounts.Keys.Max() + 1 : 1;
|
||||
var acc = new AccountState { AccountId = newId, Name = "Neuer Account" };
|
||||
|
||||
_state.Accounts[acc.AccountId] = acc;
|
||||
_accountRepo.Upsert(acc);
|
||||
_accounts.Add(acc);
|
||||
|
||||
this.FindControl<DataGrid>("gridAccounts")!.SelectedItem = acc;
|
||||
Status($"Account #{newId} angelegt.");
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task DeleteAccountAsync()
|
||||
{
|
||||
if (_state == null || _accountRepo == null) return;
|
||||
if (this.FindControl<DataGrid>("gridAccounts")!.SelectedItem is not AccountState acc) return;
|
||||
|
||||
bool ok = await DialogWindow.Confirm(this, "Löschen bestätigen",
|
||||
$"Account „{acc.Name}\" (ID {acc.AccountId}) wirklich löschen?", "Löschen");
|
||||
if (!ok) return;
|
||||
|
||||
_state.Accounts.TryRemove(acc.AccountId, out _);
|
||||
_accountRepo.Delete(acc.AccountId);
|
||||
_accounts.Remove(acc);
|
||||
this.FindControl<SettingsEditor>("editorAccount")!.Show(null);
|
||||
Status($"Account #{acc.AccountId} gelöscht.");
|
||||
}
|
||||
|
||||
private void SaveSelectedAccount()
|
||||
{
|
||||
if (this.FindControl<DataGrid>("gridAccounts")!.SelectedItem is not AccountState acc) return;
|
||||
_accountRepo?.Upsert(acc);
|
||||
_state?.Accounts.AddOrUpdate(acc.AccountId, acc, (_, _) => acc);
|
||||
Status($"Account #{acc.AccountId} gespeichert um {DateTime.Now:HH:mm:ss}.");
|
||||
}
|
||||
|
||||
// ===== Master-Key (at-rest-Verschlüsselung) =====
|
||||
|
||||
private static string MasterKeyFilePath => Path.Combine(AppContext.BaseDirectory, "master.key");
|
||||
|
||||
private static bool MasterKeyExists() =>
|
||||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"))
|
||||
|| File.Exists(MasterKeyFilePath);
|
||||
|
||||
/// <summary>Schaltfläche nur aktiv, solange KEIN Master-Key existiert (Überschreiben = Lockout-Gefahr).</summary>
|
||||
private void UpdateMasterKeyButtonState() =>
|
||||
this.FindControl<Button>("btnGenMasterKey")!.IsEnabled = !MasterKeyExists();
|
||||
|
||||
private async System.Threading.Tasks.Task GenerateMasterKeyAsync()
|
||||
{
|
||||
// Sicherheitsnetz gegen Race/Doppelklick: einen bestehenden Key NIEMALS überschreiben.
|
||||
if (MasterKeyExists())
|
||||
{
|
||||
await DialogWindow.Info(this, "Master-Key vorhanden",
|
||||
"Es existiert bereits ein Master-Key – Erzeugung abgebrochen. Ein Überschreiben würde den " +
|
||||
"Zugriff auf bereits verschlüsselte Wallet-Keys unwiederbringlich zerstören.");
|
||||
UpdateMasterKeyButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] keyBytes = RandomNumberGenerator.GetBytes(32); // 256-Bit-Schlüssel
|
||||
File.WriteAllText(MasterKeyFilePath, Convert.ToBase64String(keyBytes));
|
||||
RestrictToOwner(MasterKeyFilePath);
|
||||
|
||||
_logger?.Info("🔐 Master-Key erzeugt und in master.key gespeichert. At-rest-Verschlüsselung wird beim nächsten Start aktiv.");
|
||||
await DialogWindow.Info(this, "Master-Key erzeugt",
|
||||
"Ein zufälliger 32-Byte-Master-Key wurde erzeugt und in der Datei „master.key\" " +
|
||||
"(App-Ordner, gitignored) gespeichert.\n\n" +
|
||||
"WICHTIG:\n" +
|
||||
"• Sichere diese Datei SOFORT separat (Passwort-Manager / Offline-Backup).\n" +
|
||||
"• Master-Key-Verlust = KEIN Zugriff mehr auf die verschlüsselten Wallet-Keys!\n" +
|
||||
"• Die Verschlüsselung der Account-Credentials wird beim nächsten Programmstart aktiv.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DialogWindow.Info(this, "Fehler", $"Fehler beim Erzeugen des Master-Keys: {ex.Message}");
|
||||
}
|
||||
|
||||
UpdateMasterKeyButtonState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Beschränkt eine Schlüsseldatei auf den Besitzer. Auf Linux legt <see cref="File.WriteAllText"/>
|
||||
/// mit der üblichen umask sonst <c>644</c> an – die Datei wäre für alle lesbar. Unter Windows
|
||||
/// greift die NTFS-Vererbung, dort ist nichts zu tun.
|
||||
/// </summary>
|
||||
private void RestrictToOwner(string path)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
|
||||
try
|
||||
{
|
||||
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Warning($"⚠️ Dateirechte für {Path.GetFileName(path)} konnten nicht gesetzt werden: {ex.Message}. " +
|
||||
"Bitte manuell prüfen (chmod 600).");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== OpenRouter-API-Key (Supervisor) =====
|
||||
|
||||
private async System.Threading.Tasks.Task SetOpenRouterKeyAsync()
|
||||
{
|
||||
string keyFile = Path.Combine(AppContext.BaseDirectory, "openrouter.key");
|
||||
bool exists = File.Exists(keyFile) ||
|
||||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("POLYTRADER_OPENROUTER_KEY"));
|
||||
|
||||
string? key = await DialogWindow.Prompt(this, "OpenRouter-API-Key",
|
||||
"OpenRouter-API-Key für den Supervisor eingeben.\n" +
|
||||
"Wird in der gitignorierten Datei „openrouter.key\" gespeichert (leer = entfernen).\n" +
|
||||
(exists ? "Aktuell ist bereits ein Key hinterlegt." : "Aktuell ist KEIN Key gesetzt."));
|
||||
if (key == null) return; // Abbruch
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
if (File.Exists(keyFile)) File.Delete(keyFile);
|
||||
_logger?.Info("OpenRouter-Key aus openrouter.key entfernt.");
|
||||
Status("OpenRouter-Key entfernt.");
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllText(keyFile, key.Trim());
|
||||
RestrictToOwner(keyFile);
|
||||
_logger?.Info("OpenRouter-Key gespeichert (openrouter.key).");
|
||||
Status("OpenRouter-Key gespeichert – wirkt bei der nächsten Supervisor-Anfrage.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DialogWindow.Info(this, "Fehler", $"Fehler beim Speichern des OpenRouter-Keys: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Watchdog =====
|
||||
|
||||
private async System.Threading.Tasks.Task SetWatchdogTokenAsync()
|
||||
{
|
||||
bool exists = !string.IsNullOrWhiteSpace(_settings.WatchdogToken);
|
||||
string? token = await DialogWindow.Prompt(this, "Watchdog Agent-Token",
|
||||
"Agent-Token aus dem Watchdog-Admin eingeben.\n" +
|
||||
"Wird in der gitignorierten server_settings.xml gespeichert (leer = entfernen).\n" +
|
||||
(exists ? "Aktuell ist ein Token gesetzt." : "Aktuell ist KEIN Token gesetzt."));
|
||||
if (token == null) return; // Abbruch
|
||||
|
||||
try
|
||||
{
|
||||
// Protect() gibt ohne Master-Key den Klartext unverändert zurück – kein stiller
|
||||
// Sicherheitsverlust, es wird unten darauf hingewiesen.
|
||||
_settings.WatchdogToken = string.IsNullOrWhiteSpace(token)
|
||||
? string.Empty
|
||||
: SecretProtection.Protect(token.Trim());
|
||||
_settings.Save(SettingsPath);
|
||||
_watchdog?.ReloadSettings();
|
||||
this.FindControl<SettingsEditor>("editorServer")!.Show(_settings);
|
||||
|
||||
if (_settings.WatchdogToken.Length == 0)
|
||||
{
|
||||
_logger?.Info("Watchdog-Agent-Token entfernt.");
|
||||
Status("Watchdog-Token entfernt.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool encrypted = SecretProtection.IsEncrypted(_settings.WatchdogToken);
|
||||
_logger?.Info($"Watchdog-Agent-Token gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
|
||||
Status(encrypted
|
||||
? "Watchdog-Token gespeichert (verschlüsselt) und sofort aktiv."
|
||||
: "Watchdog-Token gespeichert – ACHTUNG: ohne Master-Key im Klartext. Mit „Master-Key erzeugen\" ändern.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DialogWindow.Info(this, "Fehler", $"Fehler beim Speichern des Watchdog-Tokens: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sendet einen Heartbeat mit den GESPEICHERTEN Einstellungen. Ungespeicherte Änderungen
|
||||
/// wirken bewusst nicht – sonst würde ein erfolgreicher Test etwas bestätigen, das im
|
||||
/// laufenden Betrieb gar nicht gilt.
|
||||
/// </summary>
|
||||
private async System.Threading.Tasks.Task TestWatchdogAsync()
|
||||
{
|
||||
if (_watchdog == null) return;
|
||||
var btn = this.FindControl<Button>("btnTestWatchdog")!;
|
||||
btn.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
Status("Sende Test-Heartbeat …");
|
||||
var result = await _watchdog.SendHeartbeatAsync("ok", "Test-Heartbeat aus dem PolyTrader-Settings-Fenster");
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_logger?.Info($"✅ Watchdog-Test-Heartbeat erfolgreich gesendet ({result.Detail}).");
|
||||
Status($"Test-Heartbeat erfolgreich ({result.Detail}). Der Monitor sollte jetzt auf „up\" stehen.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.Warning($"⚠️ Watchdog-Test-Heartbeat fehlgeschlagen: {result.Detail}");
|
||||
await DialogWindow.Info(this, "Watchdog",
|
||||
$"Test-Heartbeat fehlgeschlagen:\n\n{result.Detail}\n\n" +
|
||||
"Bitte URL, Agent-Token und Source prüfen – und die Einstellungen vorher speichern.");
|
||||
Status("Test-Heartbeat fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
btn.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user