Module im WebUI aktivieren und deaktivieren
Umsetzung von #560. Die Ansicht "Moduleinstellungen" zeigte bisher nur an, ob ein Modul laeuft. Jetzt laesst sich dort wie im Modul-Tab des Desktops festlegen, welche Module beim naechsten Start geladen werden. POST /api/modules/{name}/enabled nimmt den gewuenschten Zustand ausdruecklich entgegen ({"enabled": true|false}) und schaltet nicht einfach um. Zwei Klicks aus zwei Fenstern heben sich damit nicht auf, und ein wiederholter Aufruf aendert nichts mehr. Fehlerfaelle: - Unbekanntes Modul: 404. - Modul mit Aktivierungs-Blocker: 409. - Fehlender Zustand: 400. Die Pruefung war zuerst lueckenhaft, der Test hat es gefunden: Mit "bool Enabled" machte ein leerer Rumpf {} daraus false, und ein unvollstaendiger Aufruf haette das Modul deaktiviert. Gemeinsame Quelle statt Kopien: - KnownModules: Die Modulliste stand viermal im Code (Program, CoreViews, ModuleViews, hartkodiert im WebUI). Der Start registriert jetzt die eine Liste. - ModuleActivationView: Die Statusregeln des Desktops ("Aktiv - stoppt nach Neustart" usw.) liegen im Core. Desktop und WebUI zeigen dasselbe. ServerSettingsFile - gefunden, bevor das WebUI in die Datei schreiben durfte: ServerSettings.Load verschluckt jeden Lesefehler und liefert Vorgaben. "Frisch lesen, ein Feld aendern, speichern" haette eine gesperrte oder beschaedigte server_settings.xml durch Vorgaben ersetzt, und mit ihr Lizenzschluessel, Watchdog-Token, Mullvad-Account und Alchemy-Key. Genau so schrieb auch der Modul-Schalter des Desktops. ServerSettingsFile: - liest streng und schreibt bei unlesbarer Datei nichts, - schreibt atomar ueber eine Nachbardatei, - erhaelt unter Linux die Dateirechte, - serialisiert Aenderungen prozessweit. Der Desktop-Schalter benutzt ihn jetzt ebenfalls. Tests: Lizenzschluessel, Token und Alchemy-Key bleiben beim Umschalten in der Datei; eine kaputte Datei bleibt Byte fuer Byte unveraendert und die Anzeige bietet dann kein Umschalten an; Blocker, 404, 400, Idempotenz; die Statustabelle; strenges Lesen, atomares Schreiben und Rechte am Dateiobjekt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -288,6 +288,11 @@ namespace PolyTrader.App.Avalonia
|
||||
services.AddCorePersistence(databaseOptions);
|
||||
|
||||
services.AddSingleton(serverSettings);
|
||||
|
||||
// #560: die eine Liste aller Module (auch der nicht geladenen) und der eine Weg, die
|
||||
// Einstellungsdatei zur Laufzeit fortzuschreiben - gemeinsam fuer Desktop und WebUI.
|
||||
services.AddSingleton(new KnownModules(allModules));
|
||||
services.AddSingleton(new ServerSettingsFile(ServerSettingsPath));
|
||||
services.AddSingleton(logger);
|
||||
services.AddSingleton<TradingState>();
|
||||
services.AddSingleton<JobManager>();
|
||||
|
||||
@@ -97,13 +97,7 @@ namespace PolyTrader.App.Avalonia.Shell
|
||||
var config = services.GetRequiredService<IConfiguration>();
|
||||
|
||||
// Auch die NICHT geladenen Module auflisten – sonst liessen sie sich nie wieder aktivieren.
|
||||
var all = new IPolyTraderModule[]
|
||||
{
|
||||
new PolyTrader.Modules.CopyTrading.CopyTradingModule(),
|
||||
new PolyTrader.Modules.ResolutionFarming.ResolutionFarmingModule(),
|
||||
new PolyTrader.Modules.Supervisor.SupervisorModule(),
|
||||
new PolyTrader.Modules.Accounting.AccountingModule()
|
||||
};
|
||||
var all = services.GetRequiredService<KnownModules>().All;
|
||||
|
||||
return all.Select(m => new ModuleActivationInfo(
|
||||
m.Name,
|
||||
|
||||
@@ -114,14 +114,7 @@ namespace PolyTrader.App.Avalonia.Shell
|
||||
var config = services.GetService<IConfiguration>();
|
||||
if (config != null)
|
||||
{
|
||||
IPolyTraderModule? known = moduleName.ToLowerInvariant() switch
|
||||
{
|
||||
"copytrading" => new Modules.CopyTrading.CopyTradingModule(),
|
||||
"resolutionfarming" => new Modules.ResolutionFarming.ResolutionFarmingModule(),
|
||||
"supervisor" => new Modules.Supervisor.SupervisorModule(),
|
||||
"accounting" => new Modules.Accounting.AccountingModule(),
|
||||
_ => null
|
||||
};
|
||||
IPolyTraderModule? known = services.GetService<KnownModules>()?.Find(moduleName);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using PolyTrader.Core.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
@@ -248,34 +249,11 @@ namespace PolyTrader.App.Avalonia.Views
|
||||
_modules.Clear();
|
||||
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";
|
||||
}
|
||||
|
||||
// #560: dieselben Regeln wie im WebUI, an einer Stelle (ModuleActivationView).
|
||||
var view = ModuleActivationView.Describe(m, desiredEnabled: !disabled.Contains(m.Name));
|
||||
_modules.Add(new ModuleRow
|
||||
{
|
||||
Modul = m.Name, Status = status, Hinweis = hint, Aktion = action, CanToggle = !blocked
|
||||
Modul = view.Name, Status = view.Status, Hinweis = view.Hint, Aktion = view.Action, CanToggle = view.CanToggle
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -290,15 +268,15 @@ namespace PolyTrader.App.Avalonia.Views
|
||||
|
||||
try
|
||||
{
|
||||
// Frisch aus der Datei lesen, damit parallele Änderungen nicht überschrieben werden.
|
||||
var settings = ServerSettings.Load(_settingsPath!);
|
||||
var set = new HashSet<string>(settings.DisabledModules, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (set.Contains(info.Name)) set.Remove(info.Name);
|
||||
else set.Add(info.Name);
|
||||
|
||||
settings.DisabledModules = set.OrderBy(x => x).ToList();
|
||||
settings.Save(_settingsPath!);
|
||||
// Frisch lesen, aendern, atomar schreiben (#560). Vorher ServerSettings.Load/Save: Load
|
||||
// liefert bei einer unlesbaren Datei still Vorgaben, das anschliessende Save haette damit
|
||||
// Lizenzschluessel und Watchdog-Token ueberschrieben.
|
||||
new ServerSettingsFile(_settingsPath!).Update(settings =>
|
||||
{
|
||||
var set = new HashSet<string>(settings.DisabledModules, StringComparer.OrdinalIgnoreCase);
|
||||
if (!set.Remove(info.Name)) set.Add(info.Name);
|
||||
settings.DisabledModules = set.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
});
|
||||
|
||||
PopulateModules();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user