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)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// ─── 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user