WebUI: Server-Einstellungen ohne Geheimnisse bearbeiten
Umsetzung von #561. Entscheidung Richard, 13.09.2026: Alles, was keine Geheimnisse beruehrt, soll ueber das WebUI gehen; Geheimnisse ausschliesslich im Desktop. Eine ausdrueckliche Freigabeliste (ServerSettingsCatalog) statt Reflection. Der Desktop zeigt per Reflection alles, was ServerSettings hat. Uebernaehme das WebUI das, stuende jedes kuenftige Feld automatisch im Netz, auch ein Geheimnis. Bearbeitbar, je Feld mit Pruefung: - Farbschema und Zeitzone: nach Neustart - VPN an/aus und VPN-Standort: sofort - Blockchain-Listener: nach Neustart - Polymarket-WSS: sofort - Watchdog an/aus, Source, Instance und Intervall: sofort "Sofort" und "nach Neustart" sind am Code belegt, nicht am Feldnamen geraten. Zeitzone und Blockchain-Listener werden nur beim Start gelesen, das Farbschema haelt der ThemeManager. Nur im Desktop, Aenderungsversuch 403 und protokolliert: - Geheimnisse: Mullvad-Account, Polygon-RPC-URL (enthaelt den Alchemy-Key), Watchdog-Token, Lizenzschluessel - Watchdog-URL: an diese Adresse geht der Watchdog-Token, wer sie aendert, bekommt ihn - Mullvad-CLI-Pfad: dieses Programm wird ausgefuehrt Von den Geheimnissen zeigt das WebUI nur, ob sie gesetzt sind. OpenRouter- und Master-Key werden dafuer nicht gelesen. Gefunden dabei: MullvadVpnService baut die Kommandozeile als eine Zeichenkette ("relay set location <Wert>"). Der VPN-Standort nimmt deshalb nur Mullvad- Kennungen an (cz, cz-prg). "cz account login 999" wird abgelehnt, ein Test haelt das fest. Source und Instance gehen als JSON ans Deploymentcenter und sind auf Kennungszeichen begrenzt. POST prueft alle Werte zuerst und schreibt dann alles oder nichts, ueber ServerSettingsFile. Danach zieht er den laufenden Stand nach: das Singleton, MullvadVpnService.ReloadSettings und WatchdogHeartbeatService.ReloadSettings. Desktop - bisher schrieb er eine WebUI-Aenderung still zurueck: Die Einstellungen wurden einmal beim Oeffnen geladen, und "Speichern", "Watchdog-Token setzen" und "Lizenzschluessel setzen" schrieben jeweils das ganze alte Objekt zurueck. Jetzt: - Speichern vergleicht innerhalb der Dateisperre den Dateistand vom Laden und verweigert, wenn sich die Datei inzwischen geaendert hat. - Token und Lizenzschluessel schreiben nur noch ihr eigenes Feld. - Eine unlesbare Datei sperrt das Speichern, statt Vorgaben zu zeigen, die beim Speichern die Schluessel geloescht haetten. Die WebUI-Ansicht zeigt je Feld den Wirkungsvermerk und schickt nur, was geaendert wurde. Der Leak-Test legt seine Kanarienwerte jetzt auch in die Einstellungsdatei, sonst liefe die neue Route gegen eine leere Datei. Nicht durch Unit-Tests abgedeckt: die Desktop-Aenderung an SettingsWindow (Oberflaechenklasse). Sie wird mit der Sichtpruefung auf der VM abgenommen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -660,6 +660,7 @@ function clearLogs() {
|
||||
|
||||
// Settings View
|
||||
async function loadSettings() {
|
||||
loadServerSettings();
|
||||
try {
|
||||
const res = await fetch('/api/settings/webui');
|
||||
if (!res.ok) return;
|
||||
@@ -676,6 +677,109 @@ async function loadSettings() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ==================== Server-Einstellungen (#561) ====================
|
||||
|
||||
let serverSettingsLoaded = {};
|
||||
|
||||
async function loadServerSettings() {
|
||||
const host = document.getElementById('serverSettingsSections');
|
||||
const secrets = document.getElementById('serverSecretsList');
|
||||
if (!host) return;
|
||||
try {
|
||||
const res = await fetch('/api/settings/server');
|
||||
if (!res.ok) throw new Error(await errorText(res));
|
||||
const data = await res.json();
|
||||
|
||||
serverSettingsLoaded = {};
|
||||
data.sections.forEach(sec => sec.fields.forEach(f => { serverSettingsLoaded[f.key] = f.value; }));
|
||||
|
||||
host.innerHTML = data.sections.map(sec => `
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">${escapeHtml(sec.title)}</div>
|
||||
${sec.fields.map(renderServerField).join('')}
|
||||
</div>`).join('');
|
||||
secrets.innerHTML = data.secrets
|
||||
.map(x => `<dt>${escapeHtml(x.label)}</dt><dd>${escapeHtml(x.status)}</dd>`)
|
||||
.join('');
|
||||
} catch (e) {
|
||||
host.innerHTML = `<div class="rate-warning">Server-Einstellungen nicht ladbar: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderServerField(f) {
|
||||
const id = 'srv_' + f.key;
|
||||
const key = escapeHtml(f.key);
|
||||
const badge = !f.editable
|
||||
? '<span class="effect-badge">nur im Desktop</span>'
|
||||
: `<span class="effect-badge ${f.requiresRestart ? 'restart' : 'live'}">${f.requiresRestart ? 'nach Neustart' : 'sofort'}</span>`;
|
||||
|
||||
let control;
|
||||
if (!f.editable) {
|
||||
control = `<span class="setting-readonly">${escapeHtml(String(f.value ?? '')) || '—'}</span>`;
|
||||
} else if (f.kind === 'bool') {
|
||||
control = `<input type="checkbox" id="${id}" data-key="${key}" ${f.value ? 'checked' : ''}>`;
|
||||
} else if (f.kind === 'int') {
|
||||
control = `<input type="number" class="form-input" id="${id}" data-key="${key}" value="${escapeHtml(String(f.value))}"` +
|
||||
`${f.min != null ? ` min="${f.min}"` : ''}${f.max != null ? ` max="${f.max}"` : ''}>`;
|
||||
} else if (f.kind === 'choice') {
|
||||
control = `<select class="form-input form-select" id="${id}" data-key="${key}">` +
|
||||
(f.options || []).map(o => `<option value="${escapeHtml(o)}" ${o === f.value ? 'selected' : ''}>${escapeHtml(o)}</option>`).join('') +
|
||||
'</select>';
|
||||
} else {
|
||||
control = `<input type="text" class="form-input" id="${id}" data-key="${key}" value="${escapeHtml(String(f.value ?? ''))}">`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="setting-field">
|
||||
<label for="${id}">${escapeHtml(f.label)} ${badge}</label>
|
||||
<div>${control}</div>
|
||||
<div class="form-desc">${escapeHtml(f.description)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function saveServerSettings() {
|
||||
// Nur geaenderte Felder: Wer Zeitzone und VPN nicht angefasst hat, soll sie auch nicht mitschreiben.
|
||||
const values = {};
|
||||
document.querySelectorAll('#serverSettingsSections [data-key]').forEach(el => {
|
||||
const key = el.dataset.key;
|
||||
let v;
|
||||
if (el.type === 'checkbox') v = el.checked;
|
||||
else if (el.type === 'number') v = el.value === '' ? null : Number(el.value);
|
||||
else v = el.value;
|
||||
if (v !== serverSettingsLoaded[key]) values[key] = v;
|
||||
});
|
||||
|
||||
if (Object.keys(values).length === 0) {
|
||||
showServerSettingsMsg('Keine Änderungen.', false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settings/server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ values })
|
||||
});
|
||||
if (!res.ok) throw new Error(await errorText(res));
|
||||
const data = await res.json();
|
||||
const later = data.changed.filter(c => c.requiresRestart).map(c => c.label);
|
||||
showServerSettingsMsg(`Gespeichert: ${data.changed.map(c => c.label).join(', ')}.` +
|
||||
(later.length ? ` Erst nach einem Neustart wirksam: ${later.join(', ')}.` : ' Sofort wirksam.'), false);
|
||||
loadServerSettings();
|
||||
} catch (e) {
|
||||
showServerSettingsMsg(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function showServerSettingsMsg(text, isError) {
|
||||
const msg = document.getElementById('serverSettingsMsg');
|
||||
if (!msg) return;
|
||||
msg.textContent = text;
|
||||
msg.className = isError ? 'rate-warning' : 'form-desc';
|
||||
msg.style.margin = '0';
|
||||
msg.hidden = false;
|
||||
}
|
||||
|
||||
// Auth
|
||||
async function logout() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user