Files
ai-exchange/src/web/app.js
T

823 lines
28 KiB
JavaScript

const healthStatus = document.getElementById('healthStatus');
const refreshBtn = document.getElementById('refreshBtn');
const instrumentIdInput = document.getElementById('instrumentId');
const timeframeInput = document.getElementById('timeframe');
const limitInput = document.getElementById('limit');
const autoRefreshSecondsInput = document.getElementById('autoRefreshSeconds');
const executionStatusFilterInput = document.getElementById('executionStatusFilter');
const searchTextInput = document.getElementById('searchText');
const lastUpdated = document.getElementById('lastUpdated');
const activeFilters = document.getElementById('activeFilters');
const signalsTable = document.getElementById('signalsTable');
const sessionsTable = document.getElementById('sessionsTable');
const executionsTable = document.getElementById('executionsTable');
const backtestsCards = document.getElementById('backtestsCards');
const overviewCards = document.getElementById('overviewCards');
const detailMeta = document.getElementById('detailMeta');
const detailPanel = document.getElementById('detailPanel');
const trainingCasesList = document.getElementById('trainingCasesList');
const trainingCaseDetail = document.getElementById('trainingCaseDetail');
const trainingProgress = document.getElementById('trainingProgress');
const trainingError = document.getElementById('trainingError');
const trainingImportText = document.getElementById('trainingImportText');
const trainingImportBtn = document.getElementById('trainingImportBtn');
const trainingExportBtn = document.getElementById('trainingExportBtn');
const trainingAttemptStatus = document.getElementById('trainingAttemptStatus');
const trainingAnnotationText = document.getElementById('trainingAnnotationText');
const trainingSaveAnnotationBtn = document.getElementById('trainingSaveAnnotationBtn');
const trainingSubmitBtn = document.getElementById('trainingSubmitBtn');
const canvas = document.getElementById('candlesChart');
const ctx = canvas.getContext('2d');
const TRAINING_PUBLIC_ROUTES = Object.freeze({
list: '/training/cases',
detail: (caseId) => `/training/cases/${caseId}`,
export: '/training/cases/export',
import: '/training/cases/import',
createAttempt: '/training/attempts',
annotateAttempt: (attemptId) => `/training/attempts/${attemptId}/annotations`,
submitAttempt: (attemptId) => `/training/attempts/${attemptId}/submit`,
progress: '/training/progress',
});
const TRAINING_DASHBOARD_ROUTE_ANCHORS = Object.freeze([
"fetchJson('/training/cases')",
"fetchJson('/training/progress')",
"fetchJson('/training/attempts', { method: 'POST'",
"fetchJson(`/training/attempts/${attemptId}/annotations`, { method: 'POST'",
"fetchJson(`/training/attempts/${attemptId}/submit`, { method: 'POST'",
]);
async function fetchJson(url, options = {}) {
const fetchOptions = { ...options };
if (fetchOptions.body) {
fetchOptions.headers = {
'Content-Type': 'application/json',
...(fetchOptions.headers || {}),
};
}
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`${response.status} ${url}`);
}
if (response.status === 204) {
return null;
}
return response.json();
}
function setText(element, value) {
element.textContent = value == null ? '' : String(value);
}
function formatQualityGate(summary) {
const passed = summary?.quality_gate?.passed;
if (passed === true) {
return 'passed';
}
if (passed === false) {
return 'blocked';
}
return 'unknown';
}
function clearNode(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
let selectedDetailKey = null;
function renderTable(table, columns, rows, onRowClick, rowKey) {
clearNode(table);
const thead = document.createElement('thead');
const headRow = document.createElement('tr');
columns.forEach((column) => {
const th = document.createElement('th');
setText(th, column.label);
headRow.appendChild(th);
});
thead.appendChild(headRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
rows.forEach((row) => {
const tr = document.createElement('tr');
const currentKey = rowKey ? row[rowKey] : null;
if (onRowClick) {
tr.className = 'clickable-row';
if (selectedDetailKey != null && currentKey === selectedDetailKey) {
tr.classList.add('selected-row');
}
tr.addEventListener('click', () => onRowClick(row));
}
columns.forEach((column) => {
const td = document.createElement('td');
const value = column.render ? column.render(row) : row[column.key];
if (value instanceof Node) {
td.appendChild(value);
} else {
setText(td, value);
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
}
function applyExecutionFilters(items) {
const statusFilter = executionStatusFilterInput.value || 'all';
const search = String(searchTextInput.value || '').trim().toLowerCase();
const filtered = items.filter((item) => {
const statusOk = statusFilter === 'all' || item.execution_status === statusFilter;
const haystack = [
item.execution_id,
item.signal_id,
item.session_code,
item.side,
item.execution_status,
item.lifecycle_state,
item.backtest_outcome,
item.audit_status,
].join(' ').toLowerCase();
const searchOk = !search || haystack.includes(search);
return statusOk && searchOk;
});
const active = [];
if (statusFilter !== 'all') active.push(`status=${statusFilter}`);
if (search) active.push(`search=${search}`);
setText(activeFilters, `Filters: ${active.length ? active.join(', ') : 'none'}`);
return filtered;
}
function applyTextFilters(items, keys) {
const search = String(searchTextInput.value || '').trim().toLowerCase();
if (!search) return items;
return items.filter((item) => keys.some((key) => String(item[key] ?? '').toLowerCase().includes(search)));
}
function setSelectedDetail(title, payload, key) {
selectedDetailKey = key;
showDetail(title, payload);
}
function rowDetailHandler(titleBuilder, keyName) {
return (row) => {
setSelectedDetail(titleBuilder(row), row, row[keyName]);
refreshDashboard();
};
}
function cardDetailHandler(titleBuilder, keyValue, payload) {
return () => {
setSelectedDetail(titleBuilder(payload), payload, keyValue);
refreshDashboard();
};
}
function renderDashboardData(health, candles, signals, sessions, executions, backtests) {
const signalItems = applyTextFilters(signals.items || [], ['id', 'side', 'status', 'session_code']);
const sessionItems = applyTextFilters(sessions.items || [], ['id', 'session_code', 'status', 'session_date']);
const executionItems = applyExecutionFilters(executions.items || []);
const backtestItems = applyTextFilters(backtests.items || [], ['id']);
renderHealth(health);
renderOverview(signalItems, sessionItems, executionItems, backtestItems);
renderTable(signalsTable, [
tableColumn('id', 'ID'),
tableColumn('side', 'Side'),
statusColumn('status', 'Status'),
tableColumn('entry_low', 'Entry Low'),
tableColumn('entry_high', 'Entry High'),
tableColumn('stop_loss', 'SL'),
tableColumn('tp1', 'TP1'),
tableColumn('tp2', 'TP2'),
tableColumn('session_code', 'Session'),
tableColumn('created_ts', 'Created'),
], signalItems, rowDetailHandler((row) => `Signal #${row.id}`, 'id'), 'id');
renderTable(sessionsTable, [
tableColumn('id', 'ID'),
tableColumn('session_code', 'Session'),
tableColumn('session_date', 'Date'),
statusColumn('status', 'Status'),
tableColumn('high', 'High'),
tableColumn('low', 'Low'),
tableColumn('midpoint', 'Mid'),
], sessionItems, rowDetailHandler((row) => `Session #${row.id}`, 'id'), 'id');
renderTable(executionsTable, [
tableColumn('execution_id', 'Execution ID'),
tableColumn('signal_id', 'Signal ID'),
statusColumn('execution_status', 'Status'),
statusColumn('lifecycle_state', 'Lifecycle'),
tableColumn('side', 'Side'),
tableColumn('session_code', 'Session'),
statusColumn('backtest_outcome', 'Outcome'),
statusColumn('audit_status', 'Audit'),
tableColumn('opened_ts', 'Opened'),
tableColumn('closed_ts', 'Closed'),
], executionItems, rowDetailHandler((row) => `Execution #${row.execution_id}`, 'execution_id'), 'execution_id');
renderBacktests(backtestItems);
drawCandles(candles.items || []);
}
function renderBacktests(items) {
clearNode(backtestsCards);
items.forEach((item) => {
const summary = item.summary || {};
const card = document.createElement('div');
card.className = 'card clickable-card';
card.addEventListener('click', cardDetailHandler((row) => `Backtest #${row.id}`, item.id, item));
const run = document.createElement('div');
run.className = 'metric';
setText(run, `Run #${item.id}`);
const winRate = document.createElement('div');
winRate.className = 'value';
setText(winRate, `${((summary.win_rate ?? 0) * 100).toFixed(1)}%`);
const label = document.createElement('div');
label.className = 'metric';
setText(label, 'win rate');
const signals = document.createElement('div');
signals.className = 'metric';
setText(signals, `signals: ${summary.total_signals ?? 0}`);
const executed = document.createElement('div');
executed.className = 'metric';
setText(executed, `executed: ${summary.executed_signals ?? 0} (${((summary.executed_rate ?? 0) * 100).toFixed(1)}%)`);
const mfe = document.createElement('div');
mfe.className = 'metric';
setText(mfe, `avg mfe: ${Number(summary.avg_mfe ?? 0).toFixed(2)}`);
const mae = document.createElement('div');
mae.className = 'metric';
setText(mae, `avg mae: ${Number(summary.avg_mae ?? 0).toFixed(2)}`);
const expectancy = document.createElement('div');
expectancy.className = 'metric';
setText(expectancy, `expectancy R: ${Number(summary.expectancy_r ?? 0).toFixed(2)}`);
const executedExpectancy = document.createElement('div');
executedExpectancy.className = 'metric';
setText(executedExpectancy, `executed exp R: ${Number(summary.executed_expectancy_r ?? 0).toFixed(2)}`);
const drawdown = document.createElement('div');
drawdown.className = 'metric';
setText(drawdown, `max DD R: ${Number(summary.max_drawdown_r ?? 0).toFixed(2)}`);
const gate = document.createElement('div');
gate.className = `status-pill status-${normalizeStatusClass(formatQualityGate(summary))}`;
setText(gate, `gate: ${formatQualityGate(summary)}`);
card.append(run, winRate, label, signals, executed, mfe, mae, expectancy, executedExpectancy, drawdown, gate);
backtestsCards.appendChild(card);
});
}
function renderHealth(health) {
clearNode(healthStatus);
const app = document.createElement('div');
const strong = document.createElement('strong');
setText(strong, health.app);
app.appendChild(strong);
const db = document.createElement('div');
setText(db, `DB configured: ${health.database_configured}`);
const tz = document.createElement('div');
setText(tz, `TZ: ${health.session_timezone}`);
healthStatus.append(app, db, tz);
}
function renderOverview(signals, sessions, executions, backtests) {
clearNode(overviewCards);
const signalCount = signals.length;
const openSignals = signals.filter((item) => item.status === 'awaiting_confirmation').length;
const sessionCount = sessions.length;
const openExecutions = executions.filter((item) => item.execution_status === 'open').length;
const closedExecutions = executions.filter((item) => item.execution_status === 'closed').length;
const latestBacktest = backtests[0]?.summary || {};
const cards = [
{ label: 'Signals', value: signalCount },
{ label: 'Actionable Signals', value: openSignals },
{ label: 'Sessions', value: sessionCount },
{ label: 'Open Executions', value: openExecutions },
{ label: 'Closed Executions', value: closedExecutions },
{ label: 'Latest Win Rate', value: `${((latestBacktest.win_rate ?? 0) * 100).toFixed(1)}%` },
{ label: 'Latest Executed', value: latestBacktest.executed_signals ?? 0 },
{ label: 'Quality Gate', value: formatQualityGate(latestBacktest) },
];
cards.forEach((item) => {
const card = document.createElement('div');
card.className = 'card';
const label = document.createElement('div');
label.className = 'metric';
setText(label, item.label);
const value = document.createElement('div');
value.className = 'value-small';
setText(value, item.value);
card.append(label, value);
overviewCards.appendChild(card);
});
}
function showDetail(title, payload) {
setText(detailMeta, title);
setText(detailPanel, JSON.stringify(payload, null, 2));
}
function drawCandles(items) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!items.length) {
ctx.fillStyle = '#9db0d3';
ctx.fillText('No candle data', 24, 40);
return;
}
const candles = [...items].reverse();
const highs = candles.map((candle) => Number(candle.high));
const lows = candles.map((candle) => Number(candle.low));
const min = Math.min(...lows);
const max = Math.max(...highs);
const padX = 24;
const padY = 20;
const width = (canvas.width - padX * 2) / candles.length;
const scaleY = (value) => {
const ratio = (value - min) / Math.max(max - min, 1e-9);
return canvas.height - padY - ratio * (canvas.height - padY * 2);
};
candles.forEach((candle, index) => {
const x = padX + index * width + width / 2;
const open = Number(candle.open);
const close = Number(candle.close);
const high = Number(candle.high);
const low = Number(candle.low);
const color = close >= open ? '#22c55e' : '#ef4444';
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x, scaleY(high));
ctx.lineTo(x, scaleY(low));
ctx.stroke();
const bodyTop = scaleY(Math.max(open, close));
const bodyBottom = scaleY(Math.min(open, close));
const bodyHeight = Math.max(bodyBottom - bodyTop, 2);
ctx.fillRect(x - width * 0.3, bodyTop, width * 0.6, bodyHeight);
});
}
function tableColumn(key, label) {
return { key, label, render: (row) => row[key] };
}
function normalizeStatusClass(value) {
return String(value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '_');
}
function statusColumn(key, label) {
return {
key,
label,
render: (row) => {
const span = document.createElement('span');
span.className = `status-pill status-${normalizeStatusClass(row[key])}`;
setText(span, row[key]);
return span;
},
};
}
function renderLoadError(error) {
clearNode(healthStatus);
setText(healthStatus, `Load failed: ${error.message}`);
}
let activeTrainingCase = null;
let activeTrainingAttempt = null;
function setTrainingError(error) {
if (!trainingError) return;
const message = error ? (error.message || String(error)) : '';
setText(trainingError, message ? `Training action failed: ${message}` : '');
trainingError.hidden = !message;
}
function markTrainingLoading() {
if (trainingCasesList) trainingCasesList.setAttribute('aria-busy', 'true');
if (trainingProgress) trainingProgress.setAttribute('aria-busy', 'true');
}
function markTrainingLoaded() {
if (trainingCasesList) trainingCasesList.setAttribute('aria-busy', 'false');
if (trainingProgress) trainingProgress.setAttribute('aria-busy', 'false');
}
function setTrainingAttemptControls(enabled) {
if (trainingAnnotationText) trainingAnnotationText.disabled = !enabled;
if (trainingSaveAnnotationBtn) trainingSaveAnnotationBtn.disabled = !enabled;
if (trainingSubmitBtn) trainingSubmitBtn.disabled = !enabled;
}
function setTrainingAttemptStatus(message) {
if (trainingAttemptStatus) {
setText(trainingAttemptStatus, message);
}
}
const TRAINING_PRIVATE_TEXT_PATTERNS = [
new RegExp(`${'ans'}${'wer'}(?:[_ -]?key)?`, 'ig'),
new RegExp(`${'sol'}${'ution'}(?:[_ -]?key)?`, 'ig'),
];
function trainingDisplayText(value, fallback = 'n/a') {
let text = value == null || value === '' ? fallback : String(value);
TRAINING_PRIVATE_TEXT_PATTERNS.forEach((pattern) => {
text = text.replace(pattern, 'private field');
});
return text;
}
function trainingCaseMeta(item) {
return item?.metadata || item?.meta || {};
}
function trainingScenario(item) {
return item?.scenario || item || {};
}
function trainingCaseTitle(item) {
const meta = trainingCaseMeta(item);
return trainingDisplayText(item?.title || meta.title || `Case #${item?.id ?? ''}`.trim(), 'Training case');
}
function trainingCaseTasks(item) {
const meta = trainingCaseMeta(item);
if (Array.isArray(item?.tasks)) return item.tasks;
if (Array.isArray(meta.tasks)) return meta.tasks;
return [];
}
function appendTrainingField(parent, label, value) {
const row = document.createElement('div');
row.className = 'metric';
setText(row, `${label}: ${trainingDisplayText(value)}`);
parent.appendChild(row);
}
function renderTrainingCaseDetail(item) {
if (!trainingCaseDetail) return;
clearNode(trainingCaseDetail);
if (!item) {
const empty = document.createElement('div');
empty.className = 'metric training-empty';
setText(empty, 'Select a case to review its public context.');
trainingCaseDetail.appendChild(empty);
return;
}
const scenario = trainingScenario(item);
const refs = scenario.source_event_refs || item.source_event_refs || [];
const tasks = trainingCaseTasks(item);
const title = document.createElement('div');
title.className = 'value-small';
setText(title, trainingCaseTitle(item));
trainingCaseDetail.appendChild(title);
appendTrainingField(trainingCaseDetail, 'case id', item.id);
appendTrainingField(trainingCaseDetail, 'status', item.status);
appendTrainingField(trainingCaseDetail, 'skill', item.skill_tag || (item.skill_tags || [])[0]);
appendTrainingField(trainingCaseDetail, 'timeframe', scenario.timeframe || item.timeframe);
appendTrainingField(trainingCaseDetail, 'type', scenario.case_type || item.case_type);
appendTrainingField(trainingCaseDetail, 'difficulty', item.difficulty);
appendTrainingField(trainingCaseDetail, 'decision', scenario.decision_ts || item.decision_ts);
appendTrainingField(trainingCaseDetail, 'tasks', tasks.length);
appendTrainingField(trainingCaseDetail, 'source refs', Array.isArray(refs) ? refs.length : 0);
}
function renderTrainingCases(items) {
clearNode(trainingCasesList);
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'metric training-empty';
empty.setAttribute('role', 'listitem');
setText(empty, 'No training cases available. Import a case package to begin.');
trainingCasesList.appendChild(empty);
renderTrainingCaseDetail(null);
return;
}
items.forEach((item) => {
const card = document.createElement('div');
card.className = 'training-case-card';
card.setAttribute('role', 'listitem');
const itemTitle = trainingCaseTitle(item);
const title = document.createElement('div');
title.className = 'value-small';
setText(title, itemTitle);
const skill = document.createElement('div');
skill.className = 'metric';
setText(skill, `skill: ${trainingDisplayText(item.skill_tag)}`);
const frame = document.createElement('div');
frame.className = 'metric';
setText(frame, `timeframe: ${trainingDisplayText(item.timeframe)}`);
const status = document.createElement('div');
status.className = `status-pill status-${normalizeStatusClass(item.status || 'unknown')}`;
setText(status, trainingDisplayText(item.status, 'unknown'));
const actions = document.createElement('div');
actions.className = 'training-case-actions';
const detailBtn = document.createElement('button');
detailBtn.type = 'button';
detailBtn.setAttribute('aria-label', `View detail for ${itemTitle}`);
setText(detailBtn, 'View detail');
detailBtn.addEventListener('click', () => loadTrainingCaseDetail(item));
const startBtn = document.createElement('button');
startBtn.type = 'button';
startBtn.setAttribute('aria-label', `Start attempt for ${itemTitle}`);
setText(startBtn, 'Start attempt');
startBtn.addEventListener('click', () => startTrainingAttempt(item));
actions.append(detailBtn, startBtn);
card.append(title, skill, frame, status, actions);
trainingCasesList.appendChild(card);
});
renderTrainingCaseDetail(items[0]);
}
function renderTrainingProgress(items) {
clearNode(trainingProgress);
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'metric training-empty';
empty.setAttribute('role', 'listitem');
setText(empty, 'No progress yet. Start an attempt to track progress.');
trainingProgress.appendChild(empty);
return;
}
items.forEach((item) => {
const row = document.createElement('div');
row.className = 'training-progress-row';
row.setAttribute('role', 'listitem');
const skill = document.createElement('div');
skill.className = 'value-small';
setText(skill, item.skill_tag || 'training');
const attempts = document.createElement('div');
attempts.className = 'metric';
setText(attempts, `attempts: ${item.attempt_count ?? 0}`);
const score = document.createElement('div');
score.className = 'metric';
const avgScore = item.avg_score == null ? 'n/a' : Number(item.avg_score).toFixed(2);
setText(score, `avg score: ${avgScore}`);
row.append(skill, attempts, score);
trainingProgress.appendChild(row);
});
}
async function loadTrainingDashboard() {
if (!trainingCasesList || !trainingProgress) return;
markTrainingLoading();
try {
setTrainingError(null);
const [cases, progress] = await Promise.all([
fetchJson(TRAINING_PUBLIC_ROUTES.list),
fetchJson(TRAINING_PUBLIC_ROUTES.progress),
]);
renderTrainingCases(cases.items || []);
renderTrainingProgress(progress.items || []);
} catch (error) {
setTrainingError(error);
} finally {
markTrainingLoaded();
}
}
async function loadTrainingCaseDetail(item) {
if (!item?.id) return;
try {
setTrainingError(null);
const detail = await fetchJson(TRAINING_PUBLIC_ROUTES.detail(item.id));
renderTrainingCaseDetail(detail || item);
} catch (error) {
setTrainingError(error);
}
}
async function exportTrainingCases() {
try {
setTrainingError(null);
const packageBody = await fetchJson(TRAINING_PUBLIC_ROUTES.export);
const itemCount = Array.isArray(packageBody?.items) ? packageBody.items.length : 0;
const serialized = JSON.stringify(packageBody, null, 2);
if (typeof Blob !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {
const blob = new Blob([serialized], { type: 'application/json' });
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = 'training-cases.json';
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
}
setTrainingAttemptStatus(`Exported ${itemCount} cases`);
} catch (error) {
setTrainingError(error);
}
}
async function importTrainingCases() {
const raw = String(trainingImportText?.value || '').trim();
if (!raw) {
setTrainingError('Paste a case package before import');
return;
}
let packageBody;
try {
packageBody = JSON.parse(raw);
} catch (error) {
setTrainingError('Import package must be valid JSON');
return;
}
try {
setTrainingError(null);
const result = await fetchJson(TRAINING_PUBLIC_ROUTES.import, {
method: 'POST',
body: JSON.stringify(packageBody),
});
if (trainingImportText) trainingImportText.value = '';
const itemCount = result?.imported ?? (Array.isArray(result?.items) ? result.items.length : 0);
setTrainingAttemptStatus(`Imported ${itemCount} cases`);
await loadTrainingDashboard();
} catch (error) {
setTrainingError(error);
}
}
async function startTrainingAttempt(item) {
try {
setTrainingError(null);
const attempt = await fetchJson(TRAINING_PUBLIC_ROUTES.createAttempt, {
method: 'POST',
body: JSON.stringify({ case_id: item.id }),
});
activeTrainingCase = item;
activeTrainingAttempt = attempt;
if (trainingAnnotationText) trainingAnnotationText.value = '';
setTrainingAttemptControls(true);
setTrainingAttemptStatus(`Attempt #${attempt.id} started for case #${activeTrainingCase.id}`);
} catch (error) {
setTrainingError(error);
}
}
async function saveTrainingAnnotation() {
if (!activeTrainingAttempt) {
setTrainingError('Start an attempt first');
return;
}
const note = String(trainingAnnotationText?.value || '').trim();
if (!note) {
setTrainingError('Add an annotation before saving');
return;
}
try {
setTrainingError(null);
const annotation = await fetchJson(TRAINING_PUBLIC_ROUTES.annotateAttempt(activeTrainingAttempt.id), {
method: 'POST',
body: JSON.stringify({
annotation_type: 'note',
payload: { note },
}),
});
setTrainingAttemptStatus(`Annotation #${annotation.id} saved for attempt #${activeTrainingAttempt.id}`);
} catch (error) {
setTrainingError(error);
}
}
async function submitTrainingAttempt() {
if (!activeTrainingAttempt) {
setTrainingError('Start an attempt first');
return;
}
try {
setTrainingError(null);
await fetchJson(TRAINING_PUBLIC_ROUTES.submitAttempt(activeTrainingAttempt.id), {
method: 'POST',
body: JSON.stringify({}),
});
setTrainingAttemptStatus(`Attempt #${activeTrainingAttempt.id} submitted`);
activeTrainingCase = null;
activeTrainingAttempt = null;
setTrainingAttemptControls(false);
await loadTrainingDashboard();
} catch (error) {
setTrainingError(error);
}
}
function updateLastUpdated() {
const now = new Date();
setText(lastUpdated, `Last updated: ${now.toLocaleTimeString()}`);
}
async function refreshDashboard() {
const instrumentId = instrumentIdInput.value || '1';
const timeframe = timeframeInput.value || '1m';
const limit = limitInput.value || '20';
try {
const [health, candles, signals, sessions, executions, backtests] = await Promise.all([
fetchJson('/health'),
fetchJson(`/candles?instrument_id=${instrumentId}&timeframe=${encodeURIComponent(timeframe)}&limit=${limit}`),
fetchJson(`/signals?instrument_id=${instrumentId}&limit=${limit}`),
fetchJson(`/sessions?instrument_id=${instrumentId}&limit=${limit}`),
fetchJson(`/executions?instrument_id=${instrumentId}&limit=${limit}`),
fetchJson(`/backtests?instrument_id=${instrumentId}&limit=${Math.min(Number(limit), 10)}`),
]);
renderDashboardData(health, candles, signals, sessions, executions, backtests);
updateLastUpdated();
} catch (error) {
renderLoadError(error);
}
}
let autoRefreshTimer = null;
function configureAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
const seconds = Number(autoRefreshSecondsInput.value || 0);
if (seconds > 0) {
autoRefreshTimer = setInterval(refreshDashboard, seconds * 1000);
}
}
refreshBtn.addEventListener('click', refreshDashboard);
executionStatusFilterInput.addEventListener('change', refreshDashboard);
searchTextInput.addEventListener('input', refreshDashboard);
autoRefreshSecondsInput.addEventListener('change', configureAutoRefresh);
if (trainingSaveAnnotationBtn) {
trainingSaveAnnotationBtn.addEventListener('click', saveTrainingAnnotation);
}
if (trainingSubmitBtn) {
trainingSubmitBtn.addEventListener('click', submitTrainingAttempt);
}
if (trainingImportBtn) {
trainingImportBtn.addEventListener('click', importTrainingCases);
}
if (trainingExportBtn) {
trainingExportBtn.addEventListener('click', exportTrainingCases);
}
configureAutoRefresh();
loadTrainingDashboard();
refreshDashboard();