- 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)
305 lines
12 KiB
JavaScript
305 lines
12 KiB
JavaScript
// ─── 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, '"')}">
|
||
<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);
|
||
}
|
||
}
|
||
}
|