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:
Richard
2026-09-13 14:03:31 +02:00
co-authored by Claude Opus 5
parent 6498a3d0ec
commit 68d8d6f8d2
12 changed files with 630 additions and 88 deletions
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace PolyTrader.Core.Modularity
{
/// <summary>
/// Alle Module, die diese Installation kennt – geladen oder nicht (#560).
///
/// <para>Die Liste stand bis dahin viermal im Code: beim Start (Program), im Modul-Tab des Desktops
/// (CoreViews), bei den nicht verfügbaren Ansichten (ModuleViews) und hartkodiert im WebUI. Ein neues
/// Modul musste an allen vier Stellen nachgetragen werden. Fehlte es an einer, ließ es sich dort weder
/// sehen noch wieder aktivieren. Der Start registriert jetzt diese eine Liste; wer sie braucht, fragt
/// sie ab.</para>
/// </summary>
public sealed class KnownModules
{
public KnownModules(IEnumerable<IPolyTraderModule> modules)
{
All = (modules ?? throw new ArgumentNullException(nameof(modules))).ToList();
}
public IReadOnlyList<IPolyTraderModule> All { get; }
public IPolyTraderModule? Find(string name) =>
All.FirstOrDefault(m => string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Was ein Modul-Schalter zeigt und darf. Die Regeln stammen aus dem Modul-Tab des Desktops und liegen
/// jetzt hier, damit Desktop und WebUI dasselbe anzeigen (#560).
/// </summary>
public sealed record ModuleActivationView(
string Name,
bool IsRunning,
bool DesiredEnabled,
string? Blocker,
string Status,
string Hint,
string Action,
bool CanToggle)
{
public const string RestartHint = "Änderung greift nach Neustart";
public static ModuleActivationView Describe(ModuleActivationInfo info, bool desiredEnabled)
{
if (!string.IsNullOrEmpty(info.Blocker))
return new(info.Name, info.IsRunning, desiredEnabled, info.Blocker,
"⚠ Nicht aktivierbar", info.Blocker!, "—", false);
if (info.IsRunning && desiredEnabled)
return new(info.Name, true, true, null, "Aktiv", "", "Deaktivieren", true);
if (info.IsRunning)
return new(info.Name, true, false, null, "Aktiv – stoppt nach Neustart", RestartHint, "Aktivieren", true);
if (desiredEnabled)
return new(info.Name, false, true, null, "Startet nach Neustart", RestartHint, "Deaktivieren", true);
return new(info.Name, false, false, null, "Deaktiviert", "", "Aktivieren", true);
}
}
}