Files
IBKRTrader/src/IBKRTrader.Hosting/wwwroot/js/views/supervisor.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

305 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── Supervisor View ───
import { api } from '../api.js';
export class SupervisorView {
constructor(container, setStatus) {
this.container = container;
this.setStatus = setStatus;
this.currentSubTab = 'analysis';
this.profiles = [];
this.signals = [];
this.selectedSignal = null;
}
async render() {
this.container.innerHTML = `
<!-- Sub-Nav Tabs -->
<div style="background: #ffffff; border-bottom: 1px solid var(--border-color); display: flex; gap: 4px; padding: 0 12px;">
<button class="nav-tab active" data-subtab="analysis" id="sup-tab-analysis">Analyse</button>
<button class="nav-tab" data-subtab="dossier" id="sup-tab-dossier">Dossier-Browser</button>
<button class="nav-tab" data-subtab="reports" id="sup-tab-reports">Berichte</button>
<button class="nav-tab" data-subtab="info" id="sup-tab-info">Hinweise</button>
</div>
<div id="sup-subcontent" style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
<!-- Subtab content injected here -->
</div>
`;
const subtabs = this.container.querySelectorAll('[data-subtab]');
subtabs.forEach(tab => {
tab.addEventListener('click', () => {
subtabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
this.currentSubTab = tab.getAttribute('data-subtab');
this.renderSubTab();
});
});
await this.loadProfiles();
await this.renderSubTab();
}
async loadProfiles() {
try {
const data = await api.get('/api/modules/supervisor/profiles');
if (Array.isArray(data)) {
this.profiles = data;
}
} catch (e) {
console.warn('Konnte Supervisor-Profile nicht laden:', e);
}
}
async renderSubTab() {
const sub = document.getElementById('sup-subcontent');
if (!sub) return;
if (this.currentSubTab === 'analysis') {
sub.innerHTML = `
<div class="toolbar">
<div class="toolbar-group">
<span style="font-weight: 500;">Profil:</span>
<select id="sup-profile-select" class="select" style="min-width: 180px;">
${this.profiles.map(p => `<option value="${p}">${p}</option>`).join('')}
</select>
<button id="sup-ask-btn" class="btn btn-primary">⚡ Fragen</button>
</div>
<div id="sup-ask-status" style="color: var(--text-muted); font-size: 11px;"></div>
</div>
<div style="padding: 10px 14px; background: #ffffff; border-bottom: 1px solid var(--border-color);">
<textarea id="sup-question-input" class="input" style="width: 100%; height: 72px; font-family: var(--font-family); resize: vertical;" placeholder="Frage an den Supervisor eingeben …"></textarea>
</div>
<div id="sup-answer-container" style="flex: 1; background: #14141e; color: #d2d2d2; font-family: var(--font-mono); font-size: 12px; line-height: 1.6; padding: 14px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;">
Wählen Sie ein Profil, geben Sie eine Frage ein und klicken Sie auf 'Fragen'.
</div>
`;
document.getElementById('sup-ask-btn').addEventListener('click', () => this.ask());
document.getElementById('sup-question-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.ask();
}
});
} else if (this.currentSubTab === 'dossier') {
sub.innerHTML = `
<div style="flex: 1; display: flex; overflow: hidden;">
<!-- Left: Signals list -->
<div style="width: 420px; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; background: #ffffff;">
<div class="toolbar" style="border-top: none;">
<button id="sup-load-signals-btn" class="btn">🔄 Signale laden</button>
</div>
<div class="table-container" style="flex: 1;">
<table class="datagrid">
<thead>
<tr>
<th style="width: 100px;">Zeit</th>
<th style="width: 80px;">Symbol</th>
<th style="width: 100px;">Modul</th>
<th>Entscheidung</th>
</tr>
</thead>
<tbody id="sup-signals-tbody">
<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Signale laden …</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Right: Dossier detail markdown -->
<div style="flex: 1; display: flex; flex-direction: column; background: #14141e;">
<div style="padding: 8px 12px; background: #1e1e2d; border-bottom: 1px solid #2d2d3f; color: #a1a1aa; font-size: 11px; display: flex; justify-content: space-between;">
<span id="sup-dossier-header">Dossier-Details</span>
</div>
<div id="sup-dossier-body" style="flex: 1; overflow-y: auto; padding: 14px; color: #d2d2d2; font-family: var(--font-mono); font-size: 12px; line-height: 1.6; white-space: pre-wrap;">
Signal in der linken Tabelle auswählen.
</div>
</div>
</div>
`;
document.getElementById('sup-load-signals-btn').addEventListener('click', () => this.loadSignals());
await this.loadSignals();
} else if (this.currentSubTab === 'reports') {
sub.innerHTML = `
<div class="toolbar">
<button id="sup-load-reports-btn" class="btn">🔄 Berichte laden</button>
<div id="sup-reports-status" style="color: var(--text-muted); font-size: 11px;"></div>
</div>
<div class="table-container">
<table class="datagrid">
<thead>
<tr>
<th style="width: 140px;">Erstellt</th>
<th style="width: 120px;">Profil</th>
<th style="width: 160px;">Modell</th>
<th>Frage</th>
<th style="width: 60px; text-align: center;">Tools</th>
<th style="width: 110px;">Tokens</th>
</tr>
</thead>
<tbody id="sup-reports-tbody">
<tr><td colspan="6" style="text-align:center; color: var(--text-muted);">Lade Berichte …</td></tr>
</tbody>
</table>
</div>
`;
document.getElementById('sup-load-reports-btn').addEventListener('click', () => this.loadReports());
await this.loadReports();
} else if (this.currentSubTab === 'info') {
sub.innerHTML = `
<div style="padding: 24px; max-width: 800px; color: var(--text-main); line-height: 1.6;">
<h3 style="margin-bottom: 12px; font-size: 16px;">Supervisor Modul-Hinweise</h3>
<div id="sup-info-content">Lade Modul-Informationen …</div>
</div>
`;
await this.loadInfo();
}
}
async ask() {
const qEl = document.getElementById('sup-question-input');
const question = qEl?.value?.trim();
if (!question) {
alert('Bitte eine Frage eingeben.');
return;
}
const pEl = document.getElementById('sup-profile-select');
const profile = pEl?.value;
const btn = document.getElementById('sup-ask-btn');
const ansEl = document.getElementById('sup-answer-container');
const statusEl = document.getElementById('sup-ask-status');
btn.disabled = true;
btn.textContent = '⏳ Analysiere …';
if (statusEl) statusEl.textContent = 'Agent denkt nach …';
ansEl.textContent = 'Agent führt Tool-Aufrufe und Analysen durch … Bitte warten.';
try {
const res = await api.post('/api/modules/supervisor/ask', { question, profile });
if (res && res.success) {
let output = res.answer || '(Keine Antwort erhalten)';
if (res.toolCalls && res.toolCalls.length > 0) {
output += '\n\n── Ausgeführte Tool-Calls ──\n' +
res.toolCalls.map(tc => `• ${tc.tool}: ${JSON.stringify(tc.arguments)}`).join('\n');
}
ansEl.textContent = output;
if (statusEl) statusEl.textContent = `Fertig (${res.toolCallCount || 0} Tools genutzt).`;
} else {
ansEl.textContent = `Fehler: ${res?.message || 'Unbekannter Fehler'}`;
}
} catch (e) {
ansEl.textContent = `Fehler bei der Abfrage: ${e.message}`;
} finally {
btn.disabled = false;
btn.textContent = '⚡ Fragen';
}
}
async loadSignals() {
this.setStatus('Lade Signale …');
try {
const signals = await api.get('/api/modules/supervisor/signals?limit=100');
this.signals = Array.isArray(signals) ? signals : [];
const tbody = document.getElementById('sup-signals-tbody');
if (!tbody) return;
if (this.signals.length === 0) {
tbody.innerHTML = `<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Keine Signale gefunden.</td></tr>`;
} else {
tbody.innerHTML = this.signals.map((s, idx) => `
<tr data-idx="${idx}" class="${this.selectedSignal && this.selectedSignal.signalId === s.signalId ? 'selected' : ''}">
<td>${s.firstSeen ? s.firstSeen.substring(5, 16) : '–'}</td>
<td style="font-weight: 600;">${s.symbol || '–'}</td>
<td>${s.module || '–'}</td>
<td>${s.lastDecision || '–'}</td>
</tr>
`).join('');
tbody.querySelectorAll('tr').forEach(tr => {
tr.addEventListener('click', () => {
const idx = parseInt(tr.getAttribute('data-idx'), 10);
this.selectSignal(this.signals[idx]);
});
});
}
this.setStatus(`Signale geladen (${this.signals.length}).`);
} catch (e) {
this.setStatus(`Fehler: ${e.message}`);
}
}
async selectSignal(s) {
this.selectedSignal = s;
const headerEl = document.getElementById('sup-dossier-header');
const bodyEl = document.getElementById('sup-dossier-body');
if (!bodyEl) return;
if (headerEl) headerEl.textContent = `Dossier: ${s.symbol} (${s.signalId})`;
bodyEl.textContent = 'Lade Dossier …';
try {
const data = await api.get(`/api/modules/supervisor/dossier/${encodeURIComponent(s.signalId)}`);
if (data && data.markdown) {
bodyEl.textContent = data.markdown;
} else {
bodyEl.textContent = 'Kein Dossier für dieses Signal verfügbar.';
}
} catch (e) {
bodyEl.textContent = `Fehler beim Laden des Dossiers: ${e.message}`;
}
}
async loadReports() {
this.setStatus('Lade Berichte …');
try {
const reports = await api.get('/api/modules/supervisor/reports?limit=100');
const tbody = document.getElementById('sup-reports-tbody');
if (!tbody) return;
if (!Array.isArray(reports) || reports.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; color: var(--text-muted);">Keine Berichte vorhanden.</td></tr>`;
} else {
tbody.innerHTML = reports.map(r => `
<tr title="${(r.answer || '').replace(/"/g, '&quot;')}">
<td>${r.createdAt}</td>
<td><b>${r.profile}</b></td>
<td style="font-family: var(--font-mono); font-size: 11px;">${r.model}</td>
<td>${r.question}</td>
<td class="text-center">${r.toolCallCount}</td>
<td style="font-size: 11px;">${r.tokens}</td>
</tr>
`).join('');
}
this.setStatus(`Berichte geladen (${reports ? reports.length : 0}).`);
} catch (e) {
this.setStatus(`Fehler: ${e.message}`);
}
}
async loadInfo() {
try {
const info = await api.get('/api/modules/supervisor/info');
const el = document.getElementById('sup-info-content');
if (!el || !info) return;
el.innerHTML = `
<div style="margin-bottom: 16px; padding: 12px; border-radius: 4px; border: 1px solid ${info.openRouterKeyConfigured ? '#86efac' : '#fde68a'}; background: ${info.openRouterKeyConfigured ? '#f0fdf4' : '#fffbeb'}; color: ${info.openRouterKeyConfigured ? '#15803d' : '#b45309'};">
<b>OpenRouter API-Key:</b> ${info.openRouterKeyConfigured ? '✅ Konfiguriert' : '⚠️ Nicht gesetzt (in settings.json oder OPENROUTER_API_KEY hinterlegen)'}
</div>
<p style="margin-bottom: 8px;"><b>Zeitzone:</b> ${info.timeZone || '–'}</p>
<p style="margin-bottom: 8px;"><b>Beschreibung:</b> ${info.description || '–'}</p>
`;
} catch (e) {
console.error(e);
}
}
}