Files
IBKRTrader/src/IBKRTrader.Hosting/wwwroot/js/views/settings.js
T
Richard f923721784
Build & Test / build (ubuntu-latest) (push) Canceled after 0s
Build & Test / build (windows-latest) (push) Canceled after 0s
feat: Web-UI als zweiter Ausgabeweg neben Avalonia-Shell (#490)
- 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)
2026-09-06 19:55:40 +02:00

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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
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}`);
}
}
}