- Einheitlicher WebserverWorker in IBKRTrader.Hosting auf Kestrel-Basis - Token-Authentifizierung (AuthService) mit zeitkonstantem Vergleich und HMAC-Session-Tokens - OrderManager im Core fuer Bestands- und Reservierungsabgleich offener Broker-Orders - SettingsSchema fuer reflexionsbasiertes Auslesen und sichere Aktualisierung ohne Klartext-Passwoerter - SSE-Streaming (LogStreamService) fuer Echtzeit-Logs ueber isolierte Channels - Statische Single-Page-Application in wwwroot (Avalonia Fluent / Polytrader Theme) fuer alle 9 Ansichten - Unit-Tests fuer OrderManager, AuthService, SettingsSchema, LogStreamService und WebserverWorker (498 Tests gruen)
150 lines
5.1 KiB
JavaScript
150 lines
5.1 KiB
JavaScript
// ─── Settings View ───
|
|
|
|
import { api } from '../api.js';
|
|
|
|
export class SettingsView {
|
|
constructor(container, setStatus) {
|
|
this.container = container;
|
|
this.setStatus = setStatus;
|
|
this.sections = [];
|
|
this.changedValues = {};
|
|
}
|
|
|
|
async render() {
|
|
this.container.innerHTML = `
|
|
<div class="toolbar">
|
|
<div class="toolbar-group">
|
|
<button id="set-save-btn" class="btn btn-primary">💾 Speichern</button>
|
|
<button id="set-reload-btn" class="btn">🔄 Neu laden</button>
|
|
</div>
|
|
<div id="set-status-msg" style="color: var(--text-muted); font-size: 11px;"></div>
|
|
</div>
|
|
|
|
<div class="settings-scroll" id="settings-form-container">
|
|
<div style="text-align: center; padding: 20px; color: var(--text-muted);">Lade Einstellungen …</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('set-save-btn').addEventListener('click', () => this.save());
|
|
document.getElementById('set-reload-btn').addEventListener('click', () => this.load());
|
|
|
|
await this.load();
|
|
}
|
|
|
|
async load() {
|
|
this.setStatus('Lade Einstellungen …');
|
|
this.changedValues = {};
|
|
|
|
try {
|
|
const data = await api.get('/api/settings');
|
|
if (!Array.isArray(data)) return;
|
|
|
|
this.sections = data;
|
|
const formContainer = document.getElementById('settings-form-container');
|
|
if (!formContainer) return;
|
|
|
|
formContainer.innerHTML = data.map((section, sIdx) => `
|
|
<div class="settings-section">
|
|
<div class="settings-section-header" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'grid' : 'none'">
|
|
<span>${section.title}</span>
|
|
<span style="font-size: 11px; color: var(--text-muted);">▾</span>
|
|
</div>
|
|
<div class="settings-section-body" id="section-body-${sIdx}">
|
|
${section.fields.map(f => this.renderField(f)).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Wire input changes
|
|
data.forEach(sec => {
|
|
sec.fields.forEach(f => {
|
|
const inputEl = document.getElementById(`field-${this.escapeId(f.path)}`);
|
|
if (inputEl) {
|
|
inputEl.addEventListener('change', () => {
|
|
if (f.typeName === 'bool') {
|
|
this.changedValues[f.path] = inputEl.checked;
|
|
} else if (f.typeName === 'int' || f.typeName === 'decimal') {
|
|
this.changedValues[f.path] = Number(inputEl.value);
|
|
} else {
|
|
this.changedValues[f.path] = inputEl.value;
|
|
}
|
|
document.getElementById('set-status-msg').textContent = `${Object.keys(this.changedValues).length} Änderung(en) ungespeichert`;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
this.setStatus('Einstellungen geladen.');
|
|
document.getElementById('set-status-msg').textContent = 'Bereit.';
|
|
} catch (err) {
|
|
this.setStatus(`Fehler beim Laden: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
escapeId(path) {
|
|
return path.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
}
|
|
|
|
renderField(f) {
|
|
const id = `field-${this.escapeId(f.path)}`;
|
|
let controlHtml = '';
|
|
|
|
if (f.typeName === 'bool') {
|
|
controlHtml = `<input type="checkbox" id="${id}" ${f.value ? 'checked' : ''} style="cursor: pointer;" />`;
|
|
} else if (f.typeName === 'enum' && f.enumValues) {
|
|
controlHtml = `
|
|
<select id="${id}" style="width: 220px;">
|
|
${f.enumValues.map(opt => `<option value="${opt}" ${opt === f.value ? 'selected' : ''}>${opt}</option>`).join('')}
|
|
</select>
|
|
`;
|
|
} else if (f.isPassword) {
|
|
controlHtml = `
|
|
<input type="password" id="${id}" class="input" placeholder="${f.hasValue ? '(unverändert gespeichert)' : '(kein Wert)'}" style="width: 320px;" />
|
|
`;
|
|
} else if (f.typeName === 'int' || f.typeName === 'decimal') {
|
|
controlHtml = `
|
|
<input type="number" id="${id}" class="input" value="${f.value ?? 0}" step="${f.typeName === 'decimal' ? '0.01' : '1'}" style="width: 140px;" />
|
|
`;
|
|
} else {
|
|
controlHtml = `
|
|
<input type="text" id="${id}" class="input" value="${this.escapeHtml(f.value ?? '')}" style="width: 360px;" />
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div>
|
|
<div class="settings-label">${f.displayName}</div>
|
|
${f.description ? `<div class="settings-desc">${f.description}</div>` : ''}
|
|
</div>
|
|
<div>${controlHtml}</div>
|
|
`;
|
|
}
|
|
|
|
escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
async save() {
|
|
const count = Object.keys(this.changedValues).length;
|
|
if (count === 0) {
|
|
this.setStatus('Keine Änderungen vorhanden.');
|
|
return;
|
|
}
|
|
|
|
this.setStatus(`Speichere ${count} Einstellung(en) …`);
|
|
|
|
try {
|
|
const res = await api.post('/api/settings', { updates: this.changedValues });
|
|
this.setStatus(res.message || 'Einstellungen erfolgreich gespeichert.');
|
|
document.getElementById('set-status-msg').textContent = 'Gespeichert.';
|
|
await this.load();
|
|
} catch (err) {
|
|
this.setStatus(`Speichern fehlgeschlagen: ${err.message}`);
|
|
}
|
|
}
|
|
}
|