UI Slice 5: Menueleiste (Fenster nebeneinander), sicheres Beenden, Modul-Aktivierung

- Fenster-Menue: WindowMenu fuellt die oberste MenuStrip mit Top-Level-Eintraegen
  nebeneinander (mit Icon) statt Untermenue "Fenster"; ModuleView.Icon zentral in der
  App zugewiesen (AssignMenuIcons). Kein miFenster mehr im Launcher-Designer.
- Sicheres Beenden: ShutdownConfirmDialog (10s-Timer sperrt "Jetzt beenden", Abbrechen
  jederzeit) via IModuleUiHost.RequestShutdown(). Nur der Launcher (Hauptprozess) bietet
  "Beenden"; andere Fenster nur "Fenster schliessen" (kein App-Shutdown, Module laufen
  weiter). Launcher-Schliessen-X routet ueber dieselbe Abfrage.
- Modul-Aktivierung (restart-basiert): ServerSettings.DisabledModules, in Program.Main
  vor der DI-Registrierung gefiltert -> deaktivierte Module werden nicht geladen.
  Dashboard-Tab "Module" zeigt Status + schaltet um (Hinweis: greift nach Neustart).
  Optionaler IPolyTraderModule.GetActivationBlocker fuer "nicht aktivierbar"-Info.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-23 18:46:09 +02:00
co-authored by Claude Opus 4.8
parent 886de3a85b
commit 039bc240f8
13 changed files with 685 additions and 61 deletions
+2 -10
View File
@@ -18,7 +18,6 @@ namespace PolyTraderSharp.Ui
private void InitializeComponent()
{
menuStrip = new MenuStrip();
miFenster = new ToolStripMenuItem();
toolstrip_windows = new ToolStrip();
btn_dashboard = new ToolStripButton();
btn_settings = new ToolStripButton();
@@ -56,19 +55,13 @@ namespace PolyTraderSharp.Ui
// menuStrip
//
menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { miFenster });
// Einträge werden zur Laufzeit über WindowMenu.Wire gefüllt (alle Fenster nebeneinander mit Icon).
menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip";
menuStrip.Padding = new Padding(9, 3, 0, 3);
menuStrip.Size = new Size(2599, 35);
menuStrip.TabIndex = 0;
//
// miFenster
//
miFenster.Name = "miFenster";
miFenster.Size = new Size(85, 29);
miFenster.Text = "Fenster";
//
//
// toolstrip_windows
//
toolstrip_windows.AutoSize = false;
@@ -345,7 +338,6 @@ namespace PolyTraderSharp.Ui
#endregion
private System.Windows.Forms.MenuStrip menuStrip;
private System.Windows.Forms.ToolStripMenuItem miFenster;
private ToolStrip toolstrip_windows;
private ToolStripButton btn_settings;
private ToolStripButton btn_terminal;
+19 -2
View File
@@ -60,8 +60,9 @@ namespace PolyTraderSharp.Ui
btn.Enabled = false; // Modul/View nicht verfügbar
}
// Gemeinsames „Fenster"-Menü (Launcher ist das aktuelle Fenster → currentViewId null).
PolyTrader.Core.Modularity.WindowMenu.Wire(miFenster, _uiHost, null);
// Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das
// aktuelle Fenster → currentViewId null).
PolyTrader.Core.Modularity.WindowMenu.Wire(menuStrip, _uiHost, null);
btn_liveTrading.Click += (_, _) => CycleLiveTrading();
btn_demoTrading.Click += (_, _) => CycleDemoTrading();
@@ -86,6 +87,22 @@ namespace PolyTraderSharp.Ui
UpdateWindowButtonStates();
}
/// <summary>
/// Fängt das Schließen des Launchers ab: Statt direkt zu beenden, läuft auch das Schließen-X
/// über die Sicherheitsabfrage (siehe <see cref="ShellUiHost.RequestShutdown"/>). Erst wenn dort
/// bestätigt wurde (<see cref="ShellUiHost.ShutdownConfirmed"/>), darf das Fenster schließen.
/// </summary>
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (!_uiHost.ShutdownConfirmed)
{
e.Cancel = true;
BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
return;
}
base.OnFormClosing(e);
}
private void UpdateWindowButtonStates()
{
if (IsDisposed) return;
+38 -6
View File
@@ -17,6 +17,13 @@ namespace PolyTraderSharp.Ui
private readonly List<ModuleView> _views = new();
private readonly Dictionary<string, Form> _open = new();
private Form? _mainWindow;
private bool _shutdownDialogOpen;
/// <summary>
/// True, sobald das Herunterfahren über die Sicherheitsabfrage bestätigt wurde. Der Launcher
/// wertet das in FormClosing aus, um beim Schließen-X denselben Dialog zu erzwingen.
/// </summary>
public bool ShutdownConfirmed { get; private set; }
/// <summary>Wird ausgelöst, wenn sich der Offen-Status irgendeiner View ändert.</summary>
public event Action? OpenStateChanged;
@@ -29,15 +36,14 @@ namespace PolyTraderSharp.Ui
public void SetMainWindow(Form main) => _mainWindow = main;
/// <summary>
/// Fügt einem Fenster das gemeinsame Fenster"-Menü hinzu (oberste Leiste). Als letztes
/// hinzugefügtes Top-Control belegt der MenuStrip die oberste Zeile über etwaigen ToolStrips.
/// Fügt einem Fenster die gemeinsame Fenster-Menüleiste hinzu (oberste Leiste, alle Fenster
/// nebeneinander mit Icon). Als letztes hinzugefügtes Top-Control belegt der MenuStrip die
/// oberste Zeile über etwaigen ToolStrips.
/// </summary>
private void AttachWindowMenu(Form form, string currentViewId)
{
var menu = new MenuStrip { Dock = DockStyle.Top };
var fenster = new ToolStripMenuItem("Fenster");
menu.Items.Add(fenster);
PolyTrader.Core.Modularity.WindowMenu.Wire(fenster, this, currentViewId);
var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) };
PolyTrader.Core.Modularity.WindowMenu.Wire(menu, this, currentViewId);
form.Controls.Add(menu);
form.MainMenuStrip = menu;
}
@@ -54,6 +60,32 @@ namespace PolyTraderSharp.Ui
public bool IsOpen(string viewId) =>
_open.TryGetValue(viewId, out var form) && !form.IsDisposed;
/// <summary>
/// Zeigt die Sicherheitsabfrage (Timer-gesperrter Beenden-Button, Abbrechen jederzeit). Bei
/// Bestätigung wird die Message-Loop beendet das geordnete Herunterfahren der Module läuft
/// anschließend in Program.Main über <c>AppHost.StopAsync()</c> (nach <c>Application.Run</c>).
/// Aus jedem Fenster aufrufbar; der Dialog erscheint zentriert über dem Hauptfenster.
/// </summary>
public void RequestShutdown()
{
if (ShutdownConfirmed || _shutdownDialogOpen) return;
_shutdownDialogOpen = true;
try
{
using var dlg = new ShutdownConfirmDialog();
var owner = _mainWindow != null && !_mainWindow.IsDisposed ? _mainWindow : null;
var result = owner != null ? dlg.ShowDialog(owner) : dlg.ShowDialog();
if (result != DialogResult.OK) return;
ShutdownConfirmed = true;
Application.Exit(); // beendet die Message-Loop; StopAsync() fährt Module danach sauber herunter
}
finally
{
_shutdownDialogOpen = false;
}
}
public void OpenView(string viewId)
{
var view = _views.FirstOrDefault(v => v.Id == viewId);
+120
View File
@@ -0,0 +1,120 @@
namespace PolyTraderSharp.Ui
{
partial class ShutdownConfirmDialog
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
lblTitle = new System.Windows.Forms.Label();
lblInfo = new System.Windows.Forms.Label();
lblCountdown = new System.Windows.Forms.Label();
btnConfirm = new System.Windows.Forms.Button();
btnCancel = new System.Windows.Forms.Button();
countdownTimer = new System.Windows.Forms.Timer(components);
SuspendLayout();
//
// lblTitle
//
lblTitle.AutoSize = true;
lblTitle.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Bold);
lblTitle.Location = new System.Drawing.Point(18, 16);
lblTitle.Name = "lblTitle";
lblTitle.Size = new System.Drawing.Size(300, 28);
lblTitle.TabIndex = 0;
lblTitle.Text = "PolyTrader sicher beenden?";
//
// lblInfo
//
lblInfo.Location = new System.Drawing.Point(20, 52);
lblInfo.Name = "lblInfo";
lblInfo.Size = new System.Drawing.Size(464, 96);
lblInfo.TabIndex = 1;
lblInfo.Text = "PolyTrader und alle Module werden geordnet heruntergefahren. Laufende API-Aufrufe, "
+ "Order- und Trading-Vorgänge werden dabei sauber abgeschlossen.\r\n\r\n"
+ "Aus Sicherheit ist die Beenden-Schaltfläche erst nach Ablauf eines kurzen Timers "
+ "aktiv. Abbrechen ist jederzeit möglich.";
//
// lblCountdown
//
lblCountdown.AutoSize = true;
lblCountdown.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
lblCountdown.ForeColor = System.Drawing.Color.Firebrick;
lblCountdown.Location = new System.Drawing.Point(20, 158);
lblCountdown.Name = "lblCountdown";
lblCountdown.Size = new System.Drawing.Size(200, 23);
lblCountdown.TabIndex = 2;
lblCountdown.Text = "Beenden möglich in 10 s …";
//
// btnConfirm
//
btnConfirm.DialogResult = System.Windows.Forms.DialogResult.OK;
btnConfirm.Enabled = false;
btnConfirm.Location = new System.Drawing.Point(232, 196);
btnConfirm.Name = "btnConfirm";
btnConfirm.Size = new System.Drawing.Size(140, 36);
btnConfirm.TabIndex = 3;
btnConfirm.Text = "Beenden (10 s)";
btnConfirm.UseVisualStyleBackColor = true;
//
// btnCancel
//
btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
btnCancel.Location = new System.Drawing.Point(380, 196);
btnCancel.Name = "btnCancel";
btnCancel.Size = new System.Drawing.Size(104, 36);
btnCancel.TabIndex = 4;
btnCancel.Text = "Abbrechen";
btnCancel.UseVisualStyleBackColor = true;
//
// countdownTimer
//
countdownTimer.Interval = 1000;
countdownTimer.Tick += CountdownTimer_Tick;
//
// ShutdownConfirmDialog
//
AcceptButton = btnConfirm;
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
CancelButton = btnCancel;
ClientSize = new System.Drawing.Size(504, 248);
Controls.Add(btnCancel);
Controls.Add(btnConfirm);
Controls.Add(lblCountdown);
Controls.Add(lblInfo);
Controls.Add(lblTitle);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "ShutdownConfirmDialog";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "PolyTrader beenden";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblInfo;
private System.Windows.Forms.Label lblCountdown;
private System.Windows.Forms.Button btnConfirm;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Timer countdownTimer;
}
}
+62
View File
@@ -0,0 +1,62 @@
using System;
using System.Windows.Forms;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// Sicherheitsabfrage vor dem Herunterfahren. Die Beenden-Schaltfläche ist erst nach Ablauf eines
/// kurzen Timers klickbar (verhindert versehentliches Beenden, z.B. während auf eine API-Antwort
/// gewartet wird); „Abbrechen" ist jederzeit möglich. Bei Bestätigung liefert der Dialog
/// <see cref="DialogResult.OK"/> das eigentliche geordnete Herunterfahren übernimmt der Aufrufer.
/// </summary>
public partial class ShutdownConfirmDialog : Form
{
private int _remaining;
/// <param name="delaySeconds">Sekunden, bis „Beenden" freigeschaltet wird (Standard 10).</param>
public ShutdownConfirmDialog(int delaySeconds = 10)
{
InitializeComponent();
_remaining = Math.Max(0, delaySeconds);
UpdateCountdownUi();
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (_remaining <= 0)
{
EnableConfirm();
return;
}
countdownTimer.Start();
}
private void CountdownTimer_Tick(object? sender, EventArgs e)
{
_remaining--;
if (_remaining <= 0)
{
countdownTimer.Stop();
EnableConfirm();
return;
}
UpdateCountdownUi();
}
private void UpdateCountdownUi()
{
lblCountdown.Text = $"Beenden möglich in {_remaining} s …";
btnConfirm.Text = $"Beenden ({_remaining} s)";
}
private void EnableConfirm()
{
btnConfirm.Enabled = true;
btnConfirm.Text = "Jetzt beenden";
lblCountdown.Text = "PolyTrader kann jetzt beendet werden.";
lblCountdown.ForeColor = System.Drawing.Color.ForestGreen;
btnConfirm.Focus();
}
}
}
+98 -2
View File
@@ -58,8 +58,17 @@ namespace PolyTraderSharp.Ui.Views
lblErgebnis = new Label();
tbSearch = new TextBox();
lblSuche = new Label();
tabModules = new TabPage();
dgvModules = new DataGridView();
colModName = new DataGridViewTextBoxColumn();
colModStatus = new DataGridViewTextBoxColumn();
colModHint = new DataGridViewTextBoxColumn();
colModAction = new DataGridViewButtonColumn();
lblModulesInfo = new Label();
toolStripDash.SuspendLayout();
tabControlDash.SuspendLayout();
tabModules.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvModules).BeginInit();
tabDashboard.SuspendLayout();
((System.ComponentModel.ISupportInitialize)picEquity).BeginInit();
pnlChartsBottom.SuspendLayout();
@@ -140,6 +149,7 @@ namespace PolyTraderSharp.Ui.Views
//
tabControlDash.Controls.Add(tabDashboard);
tabControlDash.Controls.Add(tabHistory);
tabControlDash.Controls.Add(tabModules);
tabControlDash.Dock = DockStyle.Fill;
tabControlDash.Location = new Point(0, 34);
tabControlDash.Margin = new Padding(4, 5, 4, 5);
@@ -452,9 +462,86 @@ namespace PolyTraderSharp.Ui.Views
lblSuche.Size = new Size(63, 25);
lblSuche.TabIndex = 0;
lblSuche.Text = "Suche:";
//
//
// tabModules
//
tabModules.Controls.Add(dgvModules);
tabModules.Controls.Add(lblModulesInfo);
tabModules.Location = new Point(4, 34);
tabModules.Margin = new Padding(4, 5, 4, 5);
tabModules.Name = "tabModules";
tabModules.Padding = new Padding(12, 12, 12, 12);
tabModules.Size = new Size(1706, 1000);
tabModules.TabIndex = 2;
tabModules.Text = "Module";
tabModules.UseVisualStyleBackColor = true;
//
// lblModulesInfo
//
lblModulesInfo.Dock = DockStyle.Top;
lblModulesInfo.Font = new Font("Segoe UI", 10F);
lblModulesInfo.Location = new Point(12, 12);
lblModulesInfo.Name = "lblModulesInfo";
lblModulesInfo.Padding = new Padding(4, 8, 4, 12);
lblModulesInfo.Size = new Size(1682, 56);
lblModulesInfo.TabIndex = 0;
lblModulesInfo.Text = "Module aktivieren/deaktivieren. Änderungen greifen nach dem nächsten Neustart von PolyTrader.";
//
// dgvModules
//
dgvModules.AllowUserToAddRows = false;
dgvModules.AllowUserToDeleteRows = false;
dgvModules.AllowUserToResizeRows = false;
dgvModules.AutoGenerateColumns = false;
dgvModules.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvModules.Columns.AddRange(new DataGridViewColumn[] { colModName, colModStatus, colModHint, colModAction });
dgvModules.Dock = DockStyle.Fill;
dgvModules.Location = new Point(12, 68);
dgvModules.Margin = new Padding(4, 5, 4, 5);
dgvModules.Name = "dgvModules";
dgvModules.ReadOnly = true;
dgvModules.RowHeadersVisible = false;
dgvModules.RowHeadersWidth = 62;
dgvModules.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvModules.Size = new Size(1682, 920);
dgvModules.TabIndex = 1;
//
// colModName
//
colModName.DataPropertyName = "Modul";
colModName.HeaderText = "Modul";
colModName.Name = "colModName";
colModName.ReadOnly = true;
colModName.Width = 220;
//
// colModStatus
//
colModStatus.DataPropertyName = "Status";
colModStatus.HeaderText = "Status";
colModStatus.Name = "colModStatus";
colModStatus.ReadOnly = true;
colModStatus.Width = 260;
//
// colModHint
//
colModHint.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
colModHint.DataPropertyName = "Hinweis";
colModHint.HeaderText = "Hinweis";
colModHint.Name = "colModHint";
colModHint.ReadOnly = true;
//
// colModAction
//
colModAction.DataPropertyName = "Aktion";
colModAction.HeaderText = "Aktion";
colModAction.Name = "colModAction";
colModAction.ReadOnly = true;
colModAction.Text = "Umschalten";
colModAction.UseColumnTextForButtonValue = false;
colModAction.Width = 160;
//
// DashboardView
//
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1714, 1083);
@@ -477,6 +564,8 @@ namespace PolyTraderSharp.Ui.Views
((System.ComponentModel.ISupportInitialize)dgvTrades).EndInit();
pnlHistFilters.ResumeLayout(false);
pnlHistFilters.PerformLayout();
tabModules.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvModules).EndInit();
ResumeLayout(false);
PerformLayout();
}
@@ -523,5 +612,12 @@ namespace PolyTraderSharp.Ui.Views
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnlPct;
private System.Windows.Forms.DataGridViewTextBoxColumn colClosedAt;
private System.Windows.Forms.TabPage tabModules;
private System.Windows.Forms.Label lblModulesInfo;
private System.Windows.Forms.DataGridView dgvModules;
private System.Windows.Forms.DataGridViewTextBoxColumn colModName;
private System.Windows.Forms.DataGridViewTextBoxColumn colModStatus;
private System.Windows.Forms.DataGridViewTextBoxColumn colModHint;
private System.Windows.Forms.DataGridViewButtonColumn colModAction;
}
}
+142 -1
View File
@@ -22,6 +22,9 @@ namespace PolyTraderSharp.Ui.Views
private ITradeLogRepository? _tradeLog;
private TradingState? _state;
private IReadOnlyList<ModuleActivationInfo>? _moduleInfos; // alle Module (auch deaktivierte)
private string? _settingsPath; // Quelle/Ziel der Modul-Aktivierung
private List<TradeRecord> _allTrades = new(); // zuletzt geladener Roh-Satz
private List<DashboardTradeRow> _historyBase = new(); // Scope-gefiltert, Basis für die Historie-Filter
private bool _loading;
@@ -48,13 +51,24 @@ namespace PolyTraderSharp.Ui.Views
cbRange.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
tbSearch.TextChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
cbWinLoss.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
dgvModules.CellContentClick += Modules_CellContentClick;
}
public void Initialize(ITradeLogRepository tradeLog, TradingState state)
/// <param name="modules">
/// Alle bekannten Module (auch deaktivierte) für den Tab „Module". <c>null</c> = kein
/// Modul-Management (Tab wird entfernt, z.B. im Headless-Smoke-Test).
/// </param>
/// <param name="settingsPath">Pfad der Server-Settings-Datei (Ziel für die Ein/Aus-Persistenz).</param>
public void Initialize(ITradeLogRepository tradeLog, TradingState state,
IReadOnlyList<ModuleActivationInfo>? modules = null, string? settingsPath = null)
{
_tradeLog = tradeLog;
_state = state;
_moduleInfos = modules;
_settingsPath = settingsPath;
RefreshData();
InitModulesTab();
}
// ===== Daten laden / Scope =====
@@ -251,8 +265,135 @@ namespace PolyTraderSharp.Ui.Views
{
public override string ToString() => Label;
}
// ===== Module aktivieren/deaktivieren (neustart-basiert) =====
private void InitModulesTab()
{
if (_moduleInfos == null || string.IsNullOrEmpty(_settingsPath))
{
// Kein Modul-Management (z.B. Smoke-Test) -> Tab entfernen statt leer anzuzeigen.
if (tabControlDash.TabPages.Contains(tabModules))
tabControlDash.TabPages.Remove(tabModules);
return;
}
PopulateModules();
}
private HashSet<string> LoadDisabledModules()
{
try
{
var s = ServerSettings.Load(_settingsPath!);
return new HashSet<string>(s.DisabledModules, StringComparer.OrdinalIgnoreCase);
}
catch
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
}
private void PopulateModules()
{
if (_moduleInfos == null) return;
var disabled = LoadDisabledModules();
var rows = new List<ModuleRow>();
foreach (var m in _moduleInfos)
{
bool blocked = !string.IsNullOrEmpty(m.Blocker);
bool desiredEnabled = !disabled.Contains(m.Name);
string status, hint, action;
if (blocked)
{
status = "⚠ Nicht aktivierbar";
hint = m.Blocker!;
action = "—";
}
else if (m.IsRunning && desiredEnabled)
{
status = "Aktiv"; hint = ""; action = "Deaktivieren";
}
else if (m.IsRunning && !desiredEnabled)
{
status = "Aktiv stoppt nach Neustart"; hint = "Änderung greift nach Neustart"; action = "Aktivieren";
}
else if (!m.IsRunning && desiredEnabled)
{
status = "Startet nach Neustart"; hint = "Änderung greift nach Neustart"; action = "Deaktivieren";
}
else
{
status = "Deaktiviert"; hint = ""; action = "Aktivieren";
}
rows.Add(new ModuleRow { Modul = m.Name, Status = status, Hinweis = hint, Aktion = action });
}
dgvModules.DataSource = new BindingList<ModuleRow>(rows);
}
private void Modules_CellContentClick(object? sender, DataGridViewCellEventArgs e)
{
if (_moduleInfos == null || string.IsNullOrEmpty(_settingsPath)) return;
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
if (dgvModules.Columns[e.ColumnIndex].Name != "colModAction") return;
if (dgvModules.Rows[e.RowIndex].DataBoundItem is not ModuleRow row) return;
var info = _moduleInfos.FirstOrDefault(m => m.Name == row.Modul);
if (info == null) return;
if (!string.IsNullOrEmpty(info.Blocker))
{
MessageBox.Show($"Modul „{info.Name}“ kann nicht aktiviert werden:\n\n{info.Blocker}",
"Aktivierung nicht möglich", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
// Frisch aus der Datei lesen, damit parallele Änderungen (z.B. Settings-Fenster) nicht überschrieben werden.
var settings = ServerSettings.Load(_settingsPath!);
var set = new HashSet<string>(settings.DisabledModules, StringComparer.OrdinalIgnoreCase);
bool nowDisabled;
if (set.Contains(info.Name)) { set.Remove(info.Name); nowDisabled = false; }
else { set.Add(info.Name); nowDisabled = true; }
settings.DisabledModules = set.OrderBy(x => x).ToList();
settings.Save(_settingsPath!);
PopulateModules();
MessageBox.Show(
$"Modul „{info.Name}“ wird beim nächsten Start {(nowDisabled ? "NICHT mehr geladen" : "geladen")}.\n\n" +
"Die Änderung greift erst nach einem Neustart von PolyTrader.",
"Gespeichert", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Konnte die Modul-Einstellung nicht speichern: {ex.Message}", "Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>Anzeige-Zeile des Modul-Grids (Bindung über DataPropertyName).</summary>
private sealed class ModuleRow
{
public string Modul { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string Hinweis { get; set; } = string.Empty;
public string Aktion { get; set; } = string.Empty;
}
}
/// <summary>
/// Aktivierungs-Info eines Moduls fürs Dashboard: Name, ob es in dieser Session läuft und ein
/// optionaler Grund, warum es nicht aktivierbar ist (z.B. fehlender API-Key).
/// </summary>
public sealed record ModuleActivationInfo(string Name, bool IsRunning, string? Blocker);
/// <summary>Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).</summary>
public class DashboardTradeRow
{