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
+39 -20
View File
@@ -531,41 +531,60 @@ async function triggerJob(jobName) {
// Modules View
async function loadModules() {
const container = document.getElementById('modulesContainer');
try {
const res = await fetch('/api/modules');
if (!res.ok) return;
if (!res.ok) throw new Error(await errorText(res));
const modules = await res.json();
refreshNavModules(modules);
const container = document.getElementById('modulesContainer');
const views = {
copytrading: ['copytrading', 'CopyTrading öffnen →'],
resolutionfarming: ['resolutionfarming', 'Resolution Farming öffnen →'],
supervisor: ['supervisor', 'AI Supervisor öffnen →'],
accounting: ['accounting', 'Accounting & BWA öffnen →']
};
container.innerHTML = modules.map(m => {
let actionBtn = '';
if (m.name.toLowerCase() === 'copytrading') {
actionBtn = `<div style="margin-top: 14px;"><button class="btn btn-secondary btn-sm" onclick="switchView('copytrading')">CopyTrading öffnen →</button></div>`;
} else if (m.name.toLowerCase() === 'resolutionfarming') {
actionBtn = `<div style="margin-top: 14px;"><button class="btn btn-secondary btn-sm" onclick="switchView('resolutionfarming')">Resolution Farming öffnen →</button></div>`;
} else if (m.name.toLowerCase() === 'supervisor') {
actionBtn = `<div style="margin-top: 14px;"><button class="btn btn-secondary btn-sm" onclick="switchView('supervisor')">AI Supervisor öffnen →</button></div>`;
} else if (m.name.toLowerCase() === 'accounting') {
actionBtn = `<div style="margin-top: 14px;"><button class="btn btn-secondary btn-sm" onclick="switchView('accounting')">Accounting & BWA öffnen →</button></div>`;
}
const view = views[m.name.toLowerCase()];
const openBtn = view && m.isRunning
? `<button class="btn btn-secondary btn-sm" onclick="switchView(${jsArg(view[0])})">${escapeHtml(view[1])}</button>`
: '';
// #560: ausdruecklich der gewuenschte Zustand, nicht "umschalten".
const toggleBtn = m.canToggle
? `<button class="btn ${m.desiredEnabled ? 'btn-danger' : 'btn-primary'} btn-sm" onclick="setModuleEnabled(${jsArg(m.name)}, ${m.desiredEnabled ? 'false' : 'true'})">${escapeHtml(m.action)}</button>`
: '';
const statusColor = m.blocker ? 'var(--warning)' : (m.isRunning ? 'var(--positive)' : 'var(--muted)');
return `
<div class="card">
<div class="card-title">
<span>${escapeHtml(m.name)}</span>
<span style="font-size: 12px; color: ${m.isActive ? 'var(--positive)' : 'var(--muted)'}; font-weight: normal;">
● ${m.isActive ? 'Aktiv im Betrieb' : 'Deaktiviert'}
</span>
<span style="font-size: 12px; color: ${statusColor}; font-weight: normal;">● ${escapeHtml(m.status)}</span>
</div>
<div style="font-size: 13px; color: var(--muted);">
${m.isActive ? 'Modul läuft planmäßig im Hintergrund.' : 'Nicht geladen (in den Servereinstellungen oder über DisabledModules deaktiviert).'}
</div>
${actionBtn}
<div style="font-size: 13px; color: var(--muted); min-height: 18px;">${escapeHtml(m.hint || (m.isRunning ? 'Läuft planmäßig im Hintergrund.' : 'In dieser Sitzung nicht geladen.'))}</div>
<div style="margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap;">${toggleBtn}${openBtn}</div>
</div>
`;
}).join('');
} catch (e) {}
} catch (e) {
container.innerHTML = `<div class="card"><div class="card-title">Module nicht ladbar</div><div style="font-size: 13px; color: var(--negative);">${escapeHtml(e.message)}</div></div>`;
}
}
async function setModuleEnabled(name, enabled) {
if (!enabled && !confirm(`Modul ${name} deaktivieren?\n\nEs läuft bis zum nächsten Neustart weiter und wird danach nicht mehr geladen.`)) return;
try {
const res = await fetch(`/api/modules/${encodeURIComponent(name)}/enabled`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
if (!res.ok) throw new Error(await errorText(res));
} catch (e) {
alert(`Modul-Einstellung nicht gespeichert:\n\n${e.message}`);
}
loadModules();
}
// Terminal & SSE Logs