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
+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
{