feat: Web-UI als zweiter Ausgabeweg neben Avalonia-Shell (#490)
Build & Test / build (ubuntu-latest) (push) Canceled after 0s
Build & Test / build (windows-latest) (push) Canceled after 0s

- 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:
Richard
2026-09-06 19:55:40 +02:00
parent 7299148dc4
commit f923721784
35 changed files with 4856 additions and 370 deletions
+77
View File
@@ -0,0 +1,77 @@
// ─── API Client mit Authentifizierungs-Interceptor ───
export class ApiClient {
constructor() {
this.token = sessionStorage.getItem('ibkr_token') || '';
this.onUnauthorized = null;
}
setToken(token) {
this.token = token;
if (token) {
sessionStorage.setItem('ibkr_token', token);
} else {
sessionStorage.removeItem('ibkr_token');
}
}
hasToken() {
return !!this.token;
}
async request(url, options = {}) {
const headers = {
'Accept': 'application/json',
...options.headers
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
if (options.body && typeof options.body === 'object' && !(options.body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(options.body);
}
try {
const response = await fetch(url, { ...options, headers });
if (response.status === 401) {
if (this.onUnauthorized) {
this.onUnauthorized();
}
throw new Error('Authentifizierung erforderlich (401)');
}
if (!response.ok) {
let errMessage = `HTTP ${response.status}`;
try {
const errData = await response.json();
if (errData.error || errData.message) {
errMessage = errData.error || errData.message;
}
} catch (_) {}
throw new Error(errMessage);
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return await response.json();
}
return response;
} catch (err) {
throw err;
}
}
get(url) {
return this.request(url, { method: 'GET' });
}
post(url, body) {
return this.request(url, { method: 'POST', body });
}
}
export const api = new ApiClient();