diff --git a/Program.cs b/Program.cs
index d50b8f9..6c52dc6 100644
--- a/Program.cs
+++ b/Program.cs
@@ -27,6 +27,9 @@ internal static class Program
{
public static IHost? AppHost { get; private set; }
+ /// Pfad der (gitignorierten) Server-Settings-Datei – Quelle u.a. für die Modul-Aktivierung.
+ private const string ServerSettingsPath = "server_settings.xml";
+
[STAThread]
private static void Main(string[] args)
{
@@ -62,13 +65,21 @@ internal static class Program
ApplicationConfiguration.Initialize();
- var modules = new System.Collections.Generic.List
+ // Server-Settings früh laden (VOR der DI-Registrierung), damit deaktivierte Module gar nicht
+ // erst geladen werden. Dieselbe Instanz wird als Singleton weitergereicht (eine Quelle).
+ var serverSettings = PolyTraderSharp.Models.ServerSettings.Load(ServerSettingsPath);
+
+ // Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules).
+ var allModules = new System.Collections.Generic.List
{
new CopyTradingModule(),
new ResolutionFarmingModule(),
new SupervisorModule(),
new AccountingModule()
};
+ var disabledModules = new System.Collections.Generic.HashSet(
+ serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
+ var modules = allModules.Where(m => !disabledModules.Contains(m.Name)).ToList();
AppHost = Host.CreateDefaultBuilder()
// Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App
@@ -87,7 +98,7 @@ internal static class Program
};
services.Configure(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
- services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
+ services.AddSingleton(serverSettings);
services.AddSingleton();
services.AddSingleton(delegate(IServiceProvider sp)
{
@@ -148,10 +159,14 @@ internal static class Program
try
{
// Trade-Nummerierung fortsetzen: höchste bestehende TradeId serverseitig lesen
- // (M3: kein Full-Table-Load mehr über Find(_=>true).Max()).
- var copyState = AppHost.Services.GetRequiredService();
- var tradeLog = AppHost.Services.GetRequiredService();
- copyState.TotalCopyTrades = tradeLog.GetMaxTradeId();
+ // (M3: kein Full-Table-Load mehr über Find(_=>true).Max()). Nur wenn CopyTrading aktiv ist –
+ // sonst sind dessen Services (CopyTradingState/…) gar nicht registriert.
+ if (modules.Any(m => m is CopyTradingModule))
+ {
+ var copyState = AppHost.Services.GetRequiredService();
+ var tradeLog = AppHost.Services.GetRequiredService();
+ copyState.TotalCopyTrades = tradeLog.GetMaxTradeId();
+ }
}
catch (Exception ex)
{
@@ -223,9 +238,17 @@ internal static class Program
CreateForm = () =>
{
var view = new PolyTraderSharp.Ui.Views.DashboardView();
+ var config = viewServices.GetRequiredService();
+ // Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen.
+ var moduleInfos = allModules
+ .Select(m => new PolyTraderSharp.Ui.Views.ModuleActivationInfo(
+ m.Name, modules.Contains(m), m.GetActivationBlocker(config)))
+ .ToList();
view.Initialize(
viewServices.GetRequiredService(),
- viewServices.GetRequiredService());
+ viewServices.GetRequiredService(),
+ moduleInfos,
+ ServerSettingsPath);
return view;
}
});
@@ -235,11 +258,37 @@ internal static class Program
module.RegisterUi(uiHost, viewServices);
}
+ // Menü-Icons zentral zuweisen: Die App kennt alle Fenster-Ressourcen; Core und Module
+ // referenzieren sie nicht. So erscheint jedes Fenster mit Icon in der obersten Menüleiste.
+ AssignMenuIcons(uiHost);
+
var launcher = AppHost.Services.GetRequiredService();
Application.Run(launcher);
AppHost.StopAsync().GetAwaiter().GetResult();
}
+ ///
+ /// Weist den registrierten Views (Core + Module) ihr Menü-Icon aus den App-Ressourcen zu –
+ /// gemappt über die stabile View-ID. Bereits gesetzte Icons bleiben erhalten.
+ ///
+ private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost)
+ {
+ var map = new System.Collections.Generic.Dictionary
+ {
+ ["core.dashboard"] = Properties.Resources.dashboard,
+ ["core.settings"] = Properties.Resources.setting_tools,
+ ["core.terminal"] = Properties.Resources.error_log,
+ ["core.jobs"] = Properties.Resources.system_time,
+ ["accounting.main"] = Properties.Resources.coins_in_hand,
+ ["copytrading.main"] = Properties.Resources.cross_reference,
+ ["resolutionfarming.main"] = Properties.Resources.file_start_workflow,
+ ["supervisor.main"] = Properties.Resources.emotion_batman,
+ };
+ foreach (var view in uiHost.Views)
+ if (view.Icon == null && map.TryGetValue(view.Id, out var img))
+ view.Icon = img;
+ }
+
private static void RunConfigMigrationFromJson(string folder)
{
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
diff --git a/Ui/LauncherForm.Designer.cs b/Ui/LauncherForm.Designer.cs
index ac38dc8..c51d554 100644
--- a/Ui/LauncherForm.Designer.cs
+++ b/Ui/LauncherForm.Designer.cs
@@ -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;
diff --git a/Ui/LauncherForm.cs b/Ui/LauncherForm.cs
index 38bd3d7..6b56e78 100644
--- a/Ui/LauncherForm.cs
+++ b/Ui/LauncherForm.cs
@@ -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();
}
+ ///
+ /// Fängt das Schließen des Launchers ab: Statt direkt zu beenden, läuft auch das Schließen-X
+ /// über die Sicherheitsabfrage (siehe ). Erst wenn dort
+ /// bestätigt wurde (), darf das Fenster schließen.
+ ///
+ 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;
diff --git a/Ui/ShellUiHost.cs b/Ui/ShellUiHost.cs
index cc888b1..a473857 100644
--- a/Ui/ShellUiHost.cs
+++ b/Ui/ShellUiHost.cs
@@ -17,6 +17,13 @@ namespace PolyTraderSharp.Ui
private readonly List _views = new();
private readonly Dictionary _open = new();
private Form? _mainWindow;
+ private bool _shutdownDialogOpen;
+
+ ///
+ /// 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.
+ ///
+ public bool ShutdownConfirmed { get; private set; }
/// Wird ausgelöst, wenn sich der Offen-Status irgendeiner View ändert.
public event Action? OpenStateChanged;
@@ -29,15 +36,14 @@ namespace PolyTraderSharp.Ui
public void SetMainWindow(Form main) => _mainWindow = main;
///
- /// 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.
///
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;
+ ///
+ /// 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 AppHost.StopAsync() (nach Application.Run).
+ /// Aus jedem Fenster aufrufbar; der Dialog erscheint zentriert über dem Hauptfenster.
+ ///
+ 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);
diff --git a/Ui/ShutdownConfirmDialog.Designer.cs b/Ui/ShutdownConfirmDialog.Designer.cs
new file mode 100644
index 0000000..fbabb74
--- /dev/null
+++ b/Ui/ShutdownConfirmDialog.Designer.cs
@@ -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;
+ }
+}
diff --git a/Ui/ShutdownConfirmDialog.cs b/Ui/ShutdownConfirmDialog.cs
new file mode 100644
index 0000000..8aa28be
--- /dev/null
+++ b/Ui/ShutdownConfirmDialog.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Windows.Forms;
+
+namespace PolyTraderSharp.Ui
+{
+ ///
+ /// 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
+ /// – das eigentliche geordnete Herunterfahren übernimmt der Aufrufer.
+ ///
+ public partial class ShutdownConfirmDialog : Form
+ {
+ private int _remaining;
+
+ /// Sekunden, bis „Beenden" freigeschaltet wird (Standard 10).
+ 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();
+ }
+ }
+}
diff --git a/Ui/Views/DashboardView.Designer.cs b/Ui/Views/DashboardView.Designer.cs
index 14ea984..04e71ea 100644
--- a/Ui/Views/DashboardView.Designer.cs
+++ b/Ui/Views/DashboardView.Designer.cs
@@ -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;
}
}
diff --git a/Ui/Views/DashboardView.cs b/Ui/Views/DashboardView.cs
index 3ad6404..570d61f 100644
--- a/Ui/Views/DashboardView.cs
+++ b/Ui/Views/DashboardView.cs
@@ -22,6 +22,9 @@ namespace PolyTraderSharp.Ui.Views
private ITradeLogRepository? _tradeLog;
private TradingState? _state;
+ private IReadOnlyList? _moduleInfos; // alle Module (auch deaktivierte)
+ private string? _settingsPath; // Quelle/Ziel der Modul-Aktivierung
+
private List _allTrades = new(); // zuletzt geladener Roh-Satz
private List _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)
+ ///
+ /// Alle bekannten Module (auch deaktivierte) für den Tab „Module". null = kein
+ /// Modul-Management (Tab wird entfernt, z.B. im Headless-Smoke-Test).
+ ///
+ /// Pfad der Server-Settings-Datei (Ziel für die Ein/Aus-Persistenz).
+ public void Initialize(ITradeLogRepository tradeLog, TradingState state,
+ IReadOnlyList? 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 LoadDisabledModules()
+ {
+ try
+ {
+ var s = ServerSettings.Load(_settingsPath!);
+ return new HashSet(s.DisabledModules, StringComparer.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ private void PopulateModules()
+ {
+ if (_moduleInfos == null) return;
+ var disabled = LoadDisabledModules();
+
+ var rows = new List();
+ 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(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(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);
+ }
+ }
+
+ /// Anzeige-Zeile des Modul-Grids (Bindung über DataPropertyName).
+ 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;
+ }
}
+ ///
+ /// 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).
+ ///
+ public sealed record ModuleActivationInfo(string Name, bool IsRunning, string? Blocker);
+
/// Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).
public class DashboardTradeRow
{
diff --git a/src/PolyTrader.Core/Models/ServerSettings.cs b/src/PolyTrader.Core/Models/ServerSettings.cs
index a4875a7..03351b0 100644
--- a/src/PolyTrader.Core/Models/ServerSettings.cs
+++ b/src/PolyTrader.Core/Models/ServerSettings.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Xml.Serialization;
@@ -6,6 +7,15 @@ namespace PolyTraderSharp.Models
{
public class ServerSettings
{
+ [Category("Module")]
+ [DisplayName("Deaktivierte Module")]
+ [Description("Namen der Module (z.B. \"CopyTrading\"), die beim App-Start NICHT geladen werden. " +
+ "Leer = alle Module aktiv. Änderungen greifen nach dem nächsten Neustart. " +
+ "Wird i.d.R. über das Dashboard gepflegt.")]
+ [XmlArray("DisabledModules")]
+ [XmlArrayItem("Module")]
+ public List DisabledModules { get; set; } = new();
+
[Category("Threema Notifications")]
[DisplayName("Threema Enabled")]
[Description("Enable or disable Threema notifications.")]
diff --git a/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs b/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs
index 7d07f79..87c920f 100644
--- a/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs
+++ b/src/PolyTrader.Core/Modularity/IPolyTraderModule.cs
@@ -36,6 +36,13 @@ namespace PolyTrader.Core.Modularity
///
void RegisterUi(IModuleUiHost host, IServiceProvider services);
+ ///
+ /// Optionale Vorab-Prüfung, ob das Modul aktiviert werden kann. Gibt null zurück, wenn
+ /// aktivierbar, sonst einen kurzen, im Dashboard anzeigbaren Grund (z.B. „Alchemy-API-Key fehlt").
+ /// Default: immer aktivierbar. Module überschreiben dies bei harten Voraussetzungen.
+ ///
+ string? GetActivationBlocker(IConfiguration configuration) => null;
+
/// Wird nach der Core-Hydration gestartet (kein Anlaufen gegen leeren State).
Task StartAsync(CancellationToken cancellationToken);
diff --git a/src/PolyTrader.Core/Modularity/ModuleUi.cs b/src/PolyTrader.Core/Modularity/ModuleUi.cs
index 146a747..8a45470 100644
--- a/src/PolyTrader.Core/Modularity/ModuleUi.cs
+++ b/src/PolyTrader.Core/Modularity/ModuleUi.cs
@@ -24,8 +24,12 @@ namespace PolyTrader.Core.Modularity
/// Optionale Sortierreihenfolge.
public int Order { get; init; } = 0;
- /// Optionales Icon (16x16) für Menü/Buttons. Wird von der Shell/den Menüs genutzt.
- public System.Drawing.Image? Icon { get; init; }
+ ///
+ /// Optionales Icon für Menü/Buttons. Wird von der Shell/den Menüs genutzt. Settable, damit die
+ /// App den von Modulen registrierten Views zentral ein Icon aus ihren Ressourcen zuweisen kann
+ /// (Module referenzieren die App-Ressourcen nicht).
+ ///
+ public System.Drawing.Image? Icon { get; set; }
/// Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).
public Func