<script>
// Current network (mainnet or testnet)
let currentNetwork = localStorage.getItem('network') || 'mainnet';
// API endpoints with network parameter
const getApiUrl = (endpoint) => {
const separator = endpoint.includes('?') ? '&' : '?';
return `${endpoint}${separator}network=${currentNetwork}`;
};
const API = {
account: '/api/account',
account_history: '/api/account/history',
decisions: '/api/decisions',
positions: '/api/positions',
status: '/api/status',
stats: '/api/stats',
botStatus: '/api/bot/status',
botStart: '/api/bot/start',
botPause: '/api/bot/pause',
botResume: '/api/bot/resume',
botStop: '/api/bot/stop',
debugDatabase: '/api/debug/database',
database_status: '/api/database/status'
};
let allDecisions = [];
let currentDecisionIndex = 0;
// Network switching
function switchNetwork(network) {
currentNetwork = network;
localStorage.setItem('network', network);
// Update UI
document.querySelectorAll('.network-toggle-btn').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(`network-${network}-btn`).classList.add('active');
// Reload all data
console.log(`Switched to ${network}`);
updateAll();
}
// Initialize network on load
document.addEventListener('DOMContentLoaded', () => {
switchNetwork(currentNetwork);
});
// Main tab switching
function switchMainTab(tabName) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.main-tab').forEach(tab => {
tab.classList.remove('active');
});
// Show selected tab content
document.getElementById(`tab-${tabName}`).classList.add('active');
// Add active class to clicked tab
event.target.closest('.main-tab').classList.add('active');
console.log(`Switched to ${tabName} tab`);
}
// Format currency
function formatCurrency(value) {
return '$' + value.toFixed(2);
}
// Format date/time
function formatDateTime(isoString) {
const date = new Date(isoString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Format relative time
function formatRelativeTime(isoString) {
// Ensure UTC parsing by adding 'Z' if not present
const utcString = isoString.endsWith('Z') ? isoString : isoString + 'Z';
const date = new Date(utcString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
// Get PnL class
function getPnLClass(value) {
if (value > 0) return 'positive';
if (value < 0) return 'negative';
return 'neutral';
}
// Parse justification into structured sections
function parseJustification(text) {
const sections = {
analysis: [],
risks: [],
waiting: [],
other: []
};
// Split by periods and parse numbered points
const sentences = text.split('. ');
let currentSection = 'analysis';
sentences.forEach(sentence => {
const trimmed = sentence.trim();
if (trimmed.includes('RISKS:')) {
currentSection = 'risks';
} else if (trimmed.includes('WAITING for:')) {
currentSection = 'waiting';
} else if (trimmed.match(/^\(\d+\)/)) {
// Numbered point
sections.analysis.push(trimmed);
} else if (currentSection === 'risks' && trimmed.length > 0) {
sections.risks.push(trimmed);
} else if (currentSection === 'waiting' && trimmed.length > 0) {
sections.waiting.push(trimmed);
} else if (trimmed.length > 0) {
sections.other.push(trimmed);
}
});
return sections;
}
// Create decision card HTML
function createDecisionCard(decision, isLatest = false) {
const sections = parseJustification(decision.justification);
const timestamp = formatDateTime(decision.timestamp);
const relativeTime = formatRelativeTime(decision.timestamp);
const cardClass = isLatest ? 'decision-card latest' : 'decision-card';
let html = `
<div class="${cardClass}">
<div class="decision-header">
<div class="decision-meta">
<span class="decision-coin">${decision.coin}</span>
<span class="decision-time" title="${timestamp}">${relativeTime}</span>
</div>
<div class="decision-signal-badge ${decision.signal}">
${decision.signal.toUpperCase().replace('_', ' ')}
</div>
</div>
<div class="decision-metrics">
<div class="metric">
<span class="metric-label">Confidence</span>
<span class="metric-value">${(decision.confidence * 100).toFixed(0)}%</span>
</div>
<div class="metric">
<span class="metric-label">Size</span>
<span class="metric-value">$${decision.quantity_usd.toFixed(0)}</span>
</div>
<div class="metric">
<span class="metric-label">Leverage</span>
<span class="metric-value">${decision.leverage}x</span>
</div>
`;
// Show entry price and live PnL for buy/sell/hold decisions (open positions)
if ((decision.signal === 'buy_to_enter' || decision.signal === 'sell_to_enter' || decision.signal === 'hold') && decision.entry_price) {
html += `
<div class="metric">
<span class="metric-label">Entry</span>
<span class="metric-value">$${decision.entry_price.toFixed(2)}</span>
</div>
`;
// Add live PnL if this is a position we're tracking
// The decision card will be updated dynamically with current price data
html += `
<div class="metric" id="live-pnl-${decision.id}">
<span class="metric-label">Live PnL</span>
<span class="metric-value" style="color: var(--text-muted);">Loading...</span>
</div>
`;
}
// Show exit price and PnL for close decisions
if (decision.signal === 'close' && decision.exit_price) {
html += `
<div class="metric">
<span class="metric-label">Exit</span>
<span class="metric-value">$${decision.exit_price.toFixed(0)}</span>
</div>
`;
if (decision.realized_pnl !== null && decision.realized_pnl !== undefined) {
const pnlClass = decision.realized_pnl > 0 ? 'positive' : decision.realized_pnl < 0 ? 'negative' : '';
html += `
<div class="metric">
<span class="metric-label">PnL</span>
<span class="metric-value ${pnlClass}">$${decision.realized_pnl.toFixed(2)}</span>
</div>
`;
}
}
// Continue with original metrics
html += ``;
if (decision.profit_target) {
html += `
<div class="metric">
<span class="metric-label">Target</span>
<span class="metric-value positive">$${decision.profit_target.toFixed(0)}</span>
</div>
`;
}
if (decision.stop_loss) {
html += `
<div class="metric">
<span class="metric-label">Stop</span>
<span class="metric-value negative">$${decision.stop_loss.toFixed(0)}</span>
</div>
`;
}
html += `</div>`;
// Analysis section
if (sections.analysis.length > 0) {
html += `
<div class="analysis-section">
<h4>Analysis</h4>
<ul class="analysis-points">
`;
sections.analysis.forEach(point => {
// Remove existing (N) prefix if present since <li> will auto-number
const cleanPoint = point.replace(/^\(\d+\)\s*/, '');
html += `<li>${cleanPoint}</li>`;
});
html += `</ul></div>`;
}
// Risks section
if (sections.risks.length > 0) {
html += `
<div class="risks-section">
<h4>Risks Identified</h4>
<ul class="risk-points">
`;
sections.risks.forEach(risk => {
// Remove existing (N) prefix if present since <li> will auto-number
const cleanRisk = risk.replace(/^\(\d+\)\s*/, '');
html += `<li>${cleanRisk}</li>`;
});
html += `</ul></div>`;
}
// Waiting section
if (sections.waiting.length > 0) {
html += `
<div class="waiting-section">
<h4>Waiting For</h4>
<ul class="waiting-points">
`;
sections.waiting.forEach(condition => {
// Remove existing (N) prefix if present since <li> will auto-number
const cleanCondition = condition.replace(/^\(\d+\)\s*/, '');
html += `<li>${cleanCondition}</li>`;
});
html += `</ul></div>`;
}
// Other text
if (sections.other.length > 0) {
html += `
<div class="other-analysis">
<p>${sections.other.join('. ')}</p>
</div>
`;
}
// Invalidation condition (if exists and not already in sections)
if (decision.invalidation_condition && !decision.justification.includes('invalidation')) {
html += `
<div class="invalidation">
<strong>Invalidation:</strong> ${decision.invalidation_condition}
</div>
`;
}
// Raw data button and collapsible section
const rawDataId = `raw-data-${decision.id}`;
const rawData = {
id: decision.id,
timestamp: decision.timestamp,
coin: decision.coin,
signal: decision.signal,
quantity_usd: decision.quantity_usd,
leverage: decision.leverage,
confidence: decision.confidence,
exit_plan: {
profit_target: decision.profit_target,
stop_loss: decision.stop_loss,
invalidation_condition: decision.invalidation_condition
},
justification: decision.justification
};
// Add View Prompt buttons
const userPromptId = `user-prompt-${decision.id}`;
const systemPromptId = `system-prompt-${decision.id}`;
const hasUserPrompt = decision.user_prompt && decision.user_prompt.length > 0;
const hasSystemPrompt = decision.system_prompt && decision.system_prompt.length > 0;
html += `
<div class="decision-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #eee; display: flex; gap: 8px; flex-wrap: wrap;">
${hasSystemPrompt ? `
<button class="raw-data-btn" onclick="toggleSection('${systemPromptId}')">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M7.247 11.14L2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/>
</svg>
System Prompt
</button>
` : ''}
<button class="raw-data-btn" onclick="toggleSection('${rawDataId}')">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M5.854 4.646a.5.5 0 1 0-.708.708L8.293 8.5l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5z"/>
<path d="M4.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5v-13a.5.5 0 0 0-.5-.5h-7zm0-1h7A1.5 1.5 0 0 1 13 1.5v13a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 3 14.5v-13A1.5 1.5 0 0 1 4.5 0z"/>
</svg>
Raw Response
</button>
<button class="raw-data-btn" onclick="toggleSection('supervisor-input-view')" id="supervisor-input-btn">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
</svg>
Supervisor Input
</button>
</div>
${hasUserPrompt ? `
<div class="raw-data-content" id="${userPromptId}" style="display: none;">
<h4 style="margin-top: 0; color: #333;">User Prompt (Market Data & Context):</h4>
<pre><code>${decision.user_prompt}</code></pre>
</div>
` : ''}
${hasSystemPrompt ? `
<div class="raw-data-content" id="${systemPromptId}" style="display: none;">
<h4 style="margin-top: 0; color: #333;">System Prompt (Instructions):</h4>
<pre><code>${decision.system_prompt}</code></pre>
</div>
` : ''}
<div class="raw-data-content" id="${rawDataId}" style="display: none;">
<h4 style="margin-top: 0; color: #333;">Raw JSON Response:</h4>
<pre><code>${JSON.stringify(rawData, null, 2)}</code></pre>
</div>
<div class="raw-data-content" id="supervisor-input-view" style="display: none;">
<h4 style="margin-top: 0; color: #333;">Current Supervisor Input:</h4>
<div id="supervisor-input-display-content" style="padding: 12px; background: rgba(40, 167, 69, 0.1); border-left: 4px solid #28a745; border-radius: 4px;">
<div style="color: var(--text-secondary); font-style: italic;">No active supervisor input</div>
</div>
</div>
</div>
`;
html += `</div>`;
return html;
}
// Update live PnL for the current decision if it's an open position
async function updateLivePnL(decision) {
if (!decision || !decision.entry_price) return;
if (decision.signal === 'close') return; // Already closed
const pnlElement = document.getElementById(`live-pnl-${decision.id}`);
if (!pnlElement) return;
try {
// Fetch current positions to get live price and PnL
const response = await fetch(getApiUrl(API.positions + '?status=open'));
const positions = await response.json();
// Find the position that matches this decision's coin
const position = positions.find(p => p.coin === decision.coin);
if (position && position.unrealized_pnl !== undefined) {
// Calculate current price from PnL
// PnL = (current_price - entry_price) * size * leverage (for long)
// For short: PnL = (entry_price - current_price) * size * leverage
// We can derive current_price, but we also have unrealized_pnl directly
// Calculate percentage change from PnL
// unrealized_pnl / (quantity_usd) gives us the return percentage (already accounts for leverage)
const percentChange = (position.unrealized_pnl / position.quantity_usd) * 100;
// Determine color
const isPositive = position.unrealized_pnl > 0;
const isNegative = position.unrealized_pnl < 0;
const colorClass = isPositive ? 'positive' : isNegative ? 'negative' : '';
// Update the PnL display
const valueSpan = pnlElement.querySelector('.metric-value');
valueSpan.className = `metric-value ${colorClass}`;
valueSpan.textContent = `$${position.unrealized_pnl.toFixed(2)} (${percentChange > 0 ? '+' : ''}${percentChange.toFixed(1)}%)`;
} else {
// Position not found (maybe closed), show as closed
const valueSpan = pnlElement.querySelector('.metric-value');
valueSpan.style.color = 'var(--text-muted)';
valueSpan.textContent = 'Closed';
}
} catch (error) {
console.error('Error fetching live PnL:', error);
const valueSpan = pnlElement.querySelector('.metric-value');
valueSpan.style.color = 'var(--text-muted)';
valueSpan.textContent = 'Error';
}
}
// Generic toggle function
function toggleSection(elementId) {
const element = document.getElementById(elementId);
const button = event.currentTarget; // The button that was clicked
if (!element) return;
if (element.style.display === 'none') {
element.style.display = 'block';
expandedSections.add(elementId);
button.classList.add('active');
button.style.backgroundColor = '#e9ecef';
} else {
element.style.display = 'none';
expandedSections.delete(elementId);
button.classList.remove('active');
button.style.backgroundColor = '';
}
}
// Update account stats
async function updateAccount() {
const balanceEl = document.getElementById('balance');
const realizedEl = document.getElementById('realized-pnl');
const unrealizedEl = document.getElementById('unrealized-pnl');
if (!balanceEl || !realizedEl || !unrealizedEl) {
console.log('[Account] Not on a page with account display, skipping...');
return;
}
try {
const response = await fetch(getApiUrl(API.account));
const data = await response.json();
// Show connection status in balance field
if (!data.is_connected) {
balanceEl.textContent = 'Not Connected';
balanceEl.title = data.note || 'Trading logic not yet implemented';
balanceEl.style.fontSize = '0.75em';
balanceEl.style.color = 'var(--text-muted)';
} else {
balanceEl.textContent = formatCurrency(data.balance_usd || 0);
balanceEl.title = '';
balanceEl.style.fontSize = '';
balanceEl.style.color = '';
}
// Realized PnL (from closed trades)
realizedEl.textContent = formatCurrency(data.realized_pnl || 0);
realizedEl.className = 'value ' + getPnLClass(data.realized_pnl || 0);
// Unrealized PnL (from open positions)
unrealizedEl.textContent = formatCurrency(data.unrealized_pnl || 0);
unrealizedEl.className = 'value ' + getPnLClass(data.unrealized_pnl || 0);
} catch (error) {
console.error('Error fetching account data:', error);
}
}
// Countdown timer state
let nextCycleTime = null;
let countdownInterval = null;
// Update countdown display
function updateCountdown() {
const countdownEl = document.getElementById('next-cycle-countdown');
if (!nextCycleTime) {
countdownEl.textContent = '--:--';
return;
}
const now = new Date();
const target = new Date(nextCycleTime);
const diffMs = target - now;
if (diffMs <= 0) {
countdownEl.textContent = 'Due now';
countdownEl.style.color = 'var(--color-gain)';
return;
}
const totalSeconds = Math.floor(diffMs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
// Color coding based on time remaining
if (totalSeconds < 30) {
countdownEl.style.color = 'var(--color-gain)'; // Green when close
} else {
countdownEl.style.color = 'var(--text-primary)';
}
}
// Start countdown timer
function startCountdownTimer() {
if (countdownInterval) {
clearInterval(countdownInterval);
}
countdownInterval = setInterval(updateCountdown, 1000);
updateCountdown(); // Initial update
}
// Update bot status
async function updateStatus() {
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
if (!dot || !text || !startBtn || !pauseBtn) {
console.log('[Status] Not on a page with status display, skipping...');
return;
}
try {
const response = await fetch(API.botStatus);
const data = await response.json();
// Update status indicator
const state = data.state || 'stopped';
dot.className = 'status-indicator ' + state;
text.textContent = state.charAt(0).toUpperCase() + state.slice(1);
// Update countdown timer data
if (data.next_cycle_time && state === 'running') {
nextCycleTime = data.next_cycle_time;
if (!countdownInterval) {
startCountdownTimer();
}
} else {
nextCycleTime = null;
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
document.getElementById('next-cycle-countdown').textContent = '--:--';
document.getElementById('next-cycle-countdown').style.color = 'var(--text-primary)';
}
// Update button visibility based on state
if (state === 'running') {
startBtn.style.display = 'none';
pauseBtn.style.display = 'flex';
} else if (state === 'paused') {
startBtn.style.display = 'flex';
startBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M5 3.5v9L12 8z"/>
</svg>
Resume
`;
pauseBtn.style.display = 'none';
} else {
startBtn.style.display = 'flex';
startBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M5 3.5v9L12 8z"/>
</svg>
Start
`;
pauseBtn.style.display = 'none';
}
} catch (error) {
console.error('Error fetching status:', error);
}
}
// Bot control functions
async function startBot() {
try {
const response = await fetch(API.botStart, { method: 'POST' });
const data = await response.json();
if (data.success) {
console.log('Bot started');
updateStatus();
} else {
alert('Failed to start bot: ' + data.message);
}
} catch (error) {
alert('Error starting bot: ' + error.message);
}
}
async function pauseBot() {
try {
const response = await fetch(API.botPause, { method: 'POST' });
const data = await response.json();
if (data.success) {
console.log('Bot paused');
updateStatus();
} else {
alert('Failed to pause bot: ' + data.message);
}
} catch (error) {
alert('Error pausing bot: ' + error.message);
}
}
async function resumeBot() {
try {
const response = await fetch(API.botResume, { method: 'POST' });
const data = await response.json();
if (data.success) {
console.log('Bot resumed');
updateStatus();
} else {
alert('Failed to resume bot: ' + data.message);
}
} catch (error) {
alert('Error resuming bot: ' + error.message);
}
}
async function stopBot() {
try {
const response = await fetch(API.botStop, { method: 'POST' });
const data = await response.json();
if (data.success) {
console.log('Bot stopped');
updateStatus();
} else {
alert('Failed to stop bot: ' + data.message);
}
} catch (error) {
alert('Error stopping bot: ' + error.message);
}
}
// Track expanded sections to preserve state across updates
let expandedSections = new Set();
// Update decisions
async function updateDecisions() {
const totalDecisionsEl = document.getElementById('total-decisions');
const navControls = document.getElementById('decision-nav');
if (!totalDecisionsEl || !navControls) {
console.log('[Decisions] Not on a page with decision display, skipping...');
return;
}
try {
const response = await fetch(getApiUrl(API.decisions + '?limit=50'));
const data = await response.json();
// Handle new API format (returns object with decisions array and total_count)
const newDecisions = data.decisions || data; // Fallback to array if old format
const totalCount = data.total_count || newDecisions.length;
// Only update if there's actually new data
const hasNewData = JSON.stringify(newDecisions) !== JSON.stringify(allDecisions);
if (!hasNewData && allDecisions.length > 0) {
// No changes, skip update to preserve expanded state
return;
}
allDecisions = newDecisions;
if (allDecisions.length > 0) {
// Update total decisions count (show actual total, not just fetched)
totalDecisionsEl.textContent = totalCount;
// Show navigation controls if there are multiple decisions
if (allDecisions.length > 1) {
navControls.style.display = 'flex';
} else {
navControls.style.display = 'none';
}
// Preserve current index if still valid, otherwise reset to 0
if (currentDecisionIndex >= allDecisions.length) {
currentDecisionIndex = 0;
}
// Show current decision
showDecisionAtIndex(currentDecisionIndex);
// Restore expanded sections
setTimeout(() => {
expandedSections.forEach(sectionId => {
const element = document.getElementById(sectionId);
if (element && element.style.display === 'none') {
const button = element.previousElementSibling;
if (button) button.click();
}
});
}, 100);
} else {
totalDecisionsEl.textContent = '0';
navControls.style.display = 'none';
}
} catch (error) {
console.error('Error fetching decisions:', error);
}
}
// Show decision at specific index
function showDecisionAtIndex(index) {
if (index < 0 || index >= allDecisions.length) return;
currentDecisionIndex = index;
const decision = allDecisions[index];
// Update the decision card
document.getElementById('latest-decision-card').innerHTML = createDecisionCard(decision, true);
// Update navigation controls
updateNavigationControls();
// Update live PnL if this is an open position
updateLivePnL(decision);
}
// Update navigation controls state
function updateNavigationControls() {
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const indicator = document.getElementById('nav-indicator');
// Update indicator
indicator.textContent = `${currentDecisionIndex + 1} of ${allDecisions.length}`;
// Disable/enable buttons based on position
// Previous (older) button disabled when at oldest decision
prevBtn.disabled = currentDecisionIndex === allDecisions.length - 1;
// Next (newer) button disabled when at newest decision
nextBtn.disabled = currentDecisionIndex === 0;
}
// Update positions
async function updatePositions() {
try {
// --- Open Positions ---
const response = await fetch(getApiUrl(API.positions + '?status=open'));
const data = await response.json();
const positions = data.positions || data; // Handle both {positions: [...]} and [...] formats
const list = document.getElementById('positions-list');
if (!list) {
console.log('[Positions] Not on a page with positions list, skipping...');
return;
}
if (positions.length === 0) {
list.innerHTML = `
<div style="padding: var(--spacing-md); text-align: center; color: var(--text-muted); font-size: 0.85rem;">
No open positions
</div>
`;
} else {
let html = '';
positions.forEach(pos => {
const entryTime = pos.entry_time ? formatRelativeTime(pos.entry_time) : 'Unknown';
const fullTimestamp = pos.entry_time ? new Date(pos.entry_time).toLocaleString() : '';
html += `
<div class="position-card">
<div class="position-header">
<span class="position-coin">${pos.coin}</span>
<span class="badge ${pos.side}">${pos.side.toUpperCase()}</span>
</div>
<div class="position-details">
<span>Entry: $${pos.entry_price.toFixed(2)}</span>
<span>Size: $${pos.quantity_usd.toFixed(2)}</span>
<span>Leverage: ${pos.leverage}x</span>
</div>
<div style="font-size: 0.7em; color: var(--text-muted); margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.05);" title="${fullTimestamp}">
Opened ${entryTime}
</div>
</div>
`;
});
list.innerHTML = html;
}
// Closed positions removed - now available in Database Explorer "All Positions" tab
} catch (error) {
console.error('Error fetching positions:', error);
}
}
// Update bot activity console
async function updateActivity() {
try {
// Fetch recent decisions for activity log
const decisionsResponse = await fetch(getApiUrl(API.decisions + '?limit=15'));
const decisionsData = await decisionsResponse.json();
const decisions = decisionsData.decisions || decisionsData;
// Also fetch bot status for errors
const statusResponse = await fetch(API.status);
const statusData = await statusResponse.json();
const statusHistory = statusData.history || [];
const console = document.getElementById('activity-console');
if (decisions.length === 0 && statusHistory.length === 0) {
console.innerHTML = '<div class="console-message">Waiting for bot activity...</div>';
return;
}
let html = '';
// Combine decisions and errors into activity items
const activities = [];
// Add decisions
decisions.forEach(decision => {
activities.push({
type: 'decision',
timestamp: decision.timestamp,
signal: decision.signal,
coin: decision.coin,
execution_status: decision.execution_status,
data: decision
});
});
// Add errors from bot status
statusHistory.forEach(log => {
if (log.status === 'error') {
activities.push({
type: 'error',
timestamp: log.timestamp,
message: log.error || log.message,
data: log
});
}
});
// Sort by timestamp (newest first)
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Take last 10
const recentActivities = activities.slice(0, 10);
if (recentActivities.length === 0) {
console.innerHTML = '<div class="console-message">Waiting for bot activity...</div>';
return;
}
// Render activities
recentActivities.forEach(activity => {
const relativeTime = formatRelativeTime(activity.timestamp);
const fullTimestamp = new Date(activity.timestamp).toLocaleString();
if (activity.type === 'decision') {
// Map signal to action label and color
const signalConfig = {
'buy_to_enter': { label: 'BUY', class: 'action-buy' },
'sell_to_enter': { label: 'SELL', class: 'action-sell' },
'hold': { label: 'HOLD', class: 'action-hold' },
'close': { label: 'CLOSE', class: 'action-close' },
'no_action': { label: 'NO ACTION', class: 'action-none' }
};
const config = signalConfig[activity.signal] || { label: activity.signal.toUpperCase(), class: 'action-none' };
// Debug: log unexpected signals
if (!signalConfig[activity.signal]) {
console.log('[Activity] Unknown signal:', activity.signal, 'for', activity.coin);
}
// Override with error color if execution failed
let finalClass = config.class;
if (activity.execution_status === 'failed') {
finalClass = 'action-error';
}
html += `
<div class="console-message ${finalClass}">
<div class="console-message-main">
<span class="console-time">[${relativeTime}]</span>
<span class="console-status">${config.label}</span>
<span class="console-text">${activity.coin}</span>
</div>
<div class="console-message-details">
<span class="console-timestamp">🕐 ${fullTimestamp}</span>
</div>
</div>
`;
} else if (activity.type === 'error') {
html += `
<div class="console-message action-error">
<div class="console-message-main">
<span class="console-time">[${relativeTime}]</span>
<span class="console-status">ERROR</span>
<span class="console-text">${activity.message}</span>
</div>
<div class="console-message-details">
<span class="console-timestamp">🕐 ${fullTimestamp}</span>
</div>
</div>
`;
}
});
console.innerHTML = html;
// Auto-scroll to bottom to show latest activity
console.scrollTop = console.scrollHeight;
} catch (error) {
console.error('Error fetching activity:', error);
}
}
// Load debug database data
async function loadDebugData() {
const tableSelect = document.getElementById('debug-table-select');
const container = document.getElementById('debug-entries');
// Skip if elements don't exist (legacy code)
if (!tableSelect || !container) {
console.log('[Debug] Debug table elements not found, skipping...');
return;
}
const table = tableSelect.value;
try {
container.innerHTML = '<div style="color: #6c757d;">Loading...</div>';
const response = await fetch(`${API.debugDatabase}?table=${table}&limit=3`);
const data = await response.json();
if (data.error) {
container.innerHTML = `<div style="color: #dc3545;">Error: ${data.error}</div>`;
return;
}
// Build HTML
let html = `
<div style="margin-bottom: 16px; padding-bottom: 12px; border-bottom: 2px solid #dee2e6;">
<strong>Table:</strong> ${data.table}<br>
<strong>Total Entries:</strong> ${data.total_count}<br>
<strong>Showing:</strong> ${data.entries.length} most recent
</div>
`;
if (data.entries.length === 0) {
html += '<div style="color: #6c757d; padding: 20px; text-align: center;">No entries found</div>';
} else {
data.entries.forEach((entry, index) => {
html += `
<div style="margin-bottom: 20px; padding: 16px; background: white; border-radius: 4px; border-left: 4px solid #007bff;">
<div style="font-weight: bold; margin-bottom: 12px; color: #495057;">
Entry #${entry.id || index + 1}
${entry.timestamp ? `<span style="float: right; font-weight: normal; color: #6c757d; font-size: 0.9em;">${new Date(entry.timestamp).toLocaleString()}</span>` : ''}
</div>
<pre style="background: #f8f9fa; padding: 12px; border-radius: 4px; overflow-x: auto; margin: 0; font-size: 0.85em; line-height: 1.5;"><code>${JSON.stringify(entry, null, 2)}</code></pre>
</div>
`;
});
}
container.innerHTML = html;
} catch (error) {
console.error('Error loading debug data:', error);
container.innerHTML = `<div style="color: #dc3545;">Failed to load data: ${error.message}</div>`;
}
}
// =====================================================================
// DATABASE EXPLORER TAB FUNCTIONS
// =====================================================================
// Tab switching
function switchDbTab(tabName) {
// Hide all tabs
document.querySelectorAll('.db-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.db-tab-content').forEach(content => content.classList.remove('active'));
// Show selected tab
document.querySelector(`.db-tab[onclick*="${tabName}"]`).classList.add('active');
document.getElementById(`db-tab-${tabName}`).classList.add('active');
// Load data for tab (only load if not already loaded)
if (tabName === 'overview') loadDatabaseOverview();
if (tabName === 'decisions') loadDecisionsTable();
if (tabName === 'positions') loadPositionsTable();
if (tabName === 'account') loadAccountTable();
}
// Load database overview stats
async function loadDatabaseOverview() {
// Only run if we're on the Database page (check if elements exist)
const statDecisionsEl = document.getElementById('stat-decisions');
if (!statDecisionsEl) {
console.log('[Database] Not on database page, skipping overview...');
return;
}
try {
const response = await fetch(getApiUrl(API.database_status));
const status = await response.json();
document.getElementById('stat-decisions').textContent = status.table_counts?.decisions || 0;
document.getElementById('stat-positions').textContent = status.table_counts?.positions || 0;
document.getElementById('stat-open-positions').textContent = status.table_counts?.open_positions || 0;
document.getElementById('stat-db-size').textContent = (status.database_size_mb || 0).toFixed(2);
document.getElementById('stat-db-path').textContent = status.database_path || 'Unknown';
} catch (error) {
console.error('Failed to load database overview:', error);
}
}
// Load decisions table
async function loadDecisionsTable() {
try {
const response = await fetch(getApiUrl(API.decisions + '?limit=100'));
const data = await response.json();
const decisions = data.decisions || data;
const tbody = document.getElementById('decisions-table-body');
if (decisions.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">No decisions found</td></tr>';
return;
}
tbody.innerHTML = decisions.map(d => {
const time = new Date(d.timestamp).toLocaleString('en-US', {month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
const statusBadge = d.execution_status === 'success' ? 'success' :
d.execution_status === 'failed' ? 'error' : 'pending';
return `
<tr>
<td>${time}</td>
<td>${d.coin}</td>
<td><span class="decision-signal-badge ${d.signal}">${d.signal.replace('_', ' ')}</span></td>
<td>$${d.quantity_usd?.toFixed(2) || '0.00'}</td>
<td>${d.leverage?.toFixed(1) || '1.0'}x</td>
<td>${((d.confidence || 0) * 100).toFixed(0)}%</td>
<td><span class="data-badge ${statusBadge}">${d.execution_status || 'pending'}</span></td>
</tr>
`;
}).join('');
} catch (error) {
console.error('Failed to load decisions:', error);
document.getElementById('decisions-table-body').innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--color-loss);">Error loading decisions</td></tr>';
}
}
// Load positions table
async function loadPositionsTable() {
try {
const response = await fetch(getApiUrl(API.positions + '?status=all&limit=100'));
const positions = await response.json();
const tbody = document.getElementById('positions-table-body');
if (positions.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">No positions found</td></tr>';
return;
}
tbody.innerHTML = positions.map(p => {
const entryTime = new Date(p.entry_time).toLocaleString('en-US', {month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
const exitPrice = p.exit_price ? `$${p.exit_price.toFixed(2)}` : '-';
const pnl = p.realized_pnl ? `$${p.realized_pnl.toFixed(2)}` : '-';
const pnlClass = p.realized_pnl > 0 ? 'positive' : p.realized_pnl < 0 ? 'negative' : '';
const statusBadge = p.status === 'open' ? 'pending' : 'success';
return `
<tr>
<td>${entryTime}</td>
<td>${p.coin}</td>
<td><span class="badge ${p.side}">${p.side}</span></td>
<td>$${p.entry_price?.toFixed(2) || '0.00'}</td>
<td>$${p.quantity_usd?.toFixed(2) || '0.00'}</td>
<td>${p.leverage?.toFixed(1) || '1.0'}x</td>
<td>${exitPrice}</td>
<td class="${pnlClass}">${pnl}</td>
<td><span class="data-badge ${statusBadge}">${p.status}</span></td>
</tr>
`;
}).join('');
} catch (error) {
console.error('Failed to load positions:', error);
document.getElementById('positions-table-body').innerHTML = '<tr><td colspan="9" style="text-align: center; color: var(--color-loss);">Error loading positions</td></tr>';
}
}
// Load account history table
async function loadAccountTable() {
try {
const response = await fetch(getApiUrl(API.account_history + '?limit=50'));
const history = await response.json();
const tbody = document.getElementById('account-table-body');
if (history.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">No account history found</td></tr>';
return;
}
tbody.innerHTML = history.map(h => {
const time = new Date(h.timestamp).toLocaleString('en-US', {month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
const unrealizedClass = (h.unrealized_pnl || 0) > 0 ? 'positive' : (h.unrealized_pnl || 0) < 0 ? 'negative' : '';
const realizedClass = (h.realized_pnl || 0) > 0 ? 'positive' : (h.realized_pnl || 0) < 0 ? 'negative' : '';
const totalClass = (h.total_pnl || 0) > 0 ? 'positive' : (h.total_pnl || 0) < 0 ? 'negative' : '';
// Use equity_usd if available, otherwise use balance_usd
const accountValue = h.equity_usd || h.balance_usd || 0;
return `
<tr>
<td>${time}</td>
<td>$${accountValue.toFixed(2)}</td>
<td class="${unrealizedClass}">$${h.unrealized_pnl?.toFixed(2) || '0.00'}</td>
<td class="${realizedClass}">$${h.realized_pnl?.toFixed(2) || '0.00'}</td>
<td class="${totalClass}">$${h.total_pnl?.toFixed(2) || '0.00'}</td>
<td>${h.num_positions || 0}</td>
</tr>
`;
}).join('');
} catch (error) {
console.error('Failed to load account history:', error);
document.getElementById('account-table-body').innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--color-loss);">Error loading account history</td></tr>';
}
}
// Load database status (LEGACY - keeping for compatibility)
async function loadDatabaseStatus() {
const container = document.getElementById('database-status-display');
// Skip if element doesn't exist (legacy code)
if (!container) {
console.log('[Debug] Database status display not found, skipping...');
return;
}
try {
container.innerHTML = '<div style="color: #6c757d;">Loading database status...</div>';
const response = await fetch('/api/database/status');
const data = await response.json();
if (data.error) {
container.innerHTML = `<div style="color: #dc3545;">Error: ${data.error}</div>`;
return;
}
// Build status HTML
let html = `
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">
<div>
<div style="font-weight: 600; color: #212529; margin-bottom: 4px;">Database Path</div>
<div style="color: #6c757d; font-size: 0.8rem; word-break: break-all;">${data.database_path}</div>
</div>
<div>
<div style="font-weight: 600; color: #212529; margin-bottom: 4px;">Size</div>
<div style="color: #6c757d;">${data.database_size_mb} MB (${data.database_size_bytes.toLocaleString()} bytes)</div>
</div>
</div>
<div style="border-top: 1px solid #dee2e6; padding-top: 12px; margin-top: 12px;">
<div style="font-weight: 600; color: #212529; margin-bottom: 8px;">Table Counts</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;">
<div style="background: white; padding: 8px; border-radius: 4px; border-left: 3px solid #007bff;">
<div style="font-size: 0.75rem; color: #6c757d; text-transform: uppercase;">Decisions</div>
<div style="font-size: 1.25rem; font-weight: 600; color: #212529;">${data.table_counts.decisions.toLocaleString()}</div>
</div>
<div style="background: white; padding: 8px; border-radius: 4px; border-left: 3px solid #28a745;">
<div style="font-size: 0.75rem; color: #6c757d; text-transform: uppercase;">Positions</div>
<div style="font-size: 1.25rem; font-weight: 600; color: #212529;">${data.table_counts.positions.toLocaleString()}</div>
</div>
<div style="background: white; padding: 8px; border-radius: 4px; border-left: 3px solid #ffc107;">
<div style="font-size: 0.75rem; color: #6c757d; text-transform: uppercase;">Open Positions</div>
<div style="font-size: 1.25rem; font-weight: 600; color: #212529;">${data.table_counts.open_positions.toLocaleString()}</div>
</div>
<div style="background: white; padding: 8px; border-radius: 4px; border-left: 3px solid #17a2b8;">
<div style="font-size: 0.75rem; color: #6c757d; text-transform: uppercase;">Account State</div>
<div style="font-size: 1.25rem; font-weight: 600; color: #212529;">${data.table_counts.account_state.toLocaleString()}</div>
</div>
<div style="background: white; padding: 8px; border-radius: 4px; border-left: 3px solid #6610f2;">
<div style="font-size: 0.75rem; color: #6c757d; text-transform: uppercase;">Bot Status</div>
<div style="font-size: 1.25rem; font-weight: 600; color: #212529;">${data.table_counts.bot_status.toLocaleString()}</div>
</div>
</div>
</div>
<div style="border-top: 1px solid #dee2e6; padding-top: 12px; margin-top: 12px; font-size: 0.8rem; color: #6c757d;">
<div>Last Decision: ${data.latest_timestamps.decision || 'N/A'}</div>
<div>Last Account Update: ${data.latest_timestamps.account_state || 'N/A'}</div>
<div>Status Updated: ${new Date(data.last_updated).toLocaleString()}</div>
</div>
`;
container.innerHTML = html;
} catch (error) {
console.error('Error loading database status:', error);
container.innerHTML = `<div style="color: #dc3545;">Failed to load status: ${error.message}</div>`;
}
}
// Reset database with confirmation
async function resetDatabase() {
// Confirmation dialog
const confirmed = confirm(
'⚠️ WARNING: This will permanently delete ALL data from the local database!\n\n' +
'This includes:\n' +
' • All trading decisions\n' +
' • Position history\n' +
' • Account state snapshots\n' +
' • Bot activity logs\n\n' +
'Are you absolutely sure you want to continue?'
);
if (!confirmed) {
console.log('Database reset cancelled by user');
return;
}
try {
console.log('Resetting database...');
const response = await fetch('/api/database/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
alert('✓ Database reset successfully!\n\nAll local data has been cleared.');
// Reload database status to show empty tables
await loadDatabaseStatus();
// Refresh dashboard data
await updateAll();
} else {
alert('✗ Failed to reset database:\n' + data.message);
}
} catch (error) {
console.error('Error resetting database:', error);
alert('✗ Error resetting database:\n' + error.message);
}
}
// Update all data
async function updateAll() {
await Promise.all([
updateAccount(),
updateStatus(),
updateDecisions(),
updatePositions(),
updateActivity(),
loadDatabaseOverview()
]);
// Update live PnL for the currently displayed decision
if (allDecisions.length > 0 && currentDecisionIndex >= 0) {
updateLivePnL(allDecisions[currentDecisionIndex]);
}
document.getElementById('last-update').textContent = new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
}
// Navigation buttons
// Previous = older decision (came before chronologically) = higher index
const prevBtn = document.getElementById('prev-btn');
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentDecisionIndex < allDecisions.length - 1) {
showDecisionAtIndex(currentDecisionIndex + 1);
}
});
}
// Next = newer decision (came after chronologically) = lower index
const nextBtn = document.getElementById('next-btn');
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentDecisionIndex > 0) {
showDecisionAtIndex(currentDecisionIndex - 1);
}
});
}
// Bot control buttons
const startBtn = document.getElementById('start-btn');
if (startBtn) {
startBtn.addEventListener('click', () => {
const btnText = startBtn.textContent.trim();
if (btnText === 'Resume') {
resumeBot();
} else {
startBot();
}
});
}
const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) {
pauseBtn.addEventListener('click', () => {
pauseBot();
});
}
// Initial load
updateAll();
loadDebugData(); // Load debug data on initial load
loadDatabaseStatus(); // Load database status on initial load
// Auto-refresh every 30 seconds
setInterval(updateAll, 30000);
// Auto-refresh debug data every 60 seconds
setInterval(loadDebugData, 60000);
// Auto-refresh database status every 60 seconds
setInterval(loadDatabaseStatus, 60000);
// Reload debug data when table selection changes (only if element exists)
const debugTableSelect = document.getElementById('debug-table-select');
if (debugTableSelect) {
debugTableSelect.addEventListener('change', loadDebugData);
}
// --- Trading Strategy Tab Logic ---
let currentPresetData = null;
let activePresetName = null;
let currentPromptView = 'system'; // 'system' or 'user'
async function switchPromptView(view) {
currentPromptView = view;
const editor = document.getElementById('prompt-editor');
const title = document.getElementById('prompt-view-title');
const description = document.getElementById('prompt-view-description');
const tip = document.getElementById('prompt-editor-tip');
const systemBtn = document.getElementById('view-system-btn');
const userBtn = document.getElementById('view-user-btn');
// Update button styles
if (view === 'system') {
systemBtn.style.background = 'var(--brand-primary)';
systemBtn.style.color = 'white';
userBtn.style.background = 'transparent';
userBtn.style.color = 'var(--text-muted)';
title.textContent = 'System Prompt (Claude\'s Instructions)';
description.textContent = 'This defines Claude\'s trading personality, risk tolerance, and decision-making rules';
tip.textContent = '💡 Tip: You can edit the System Prompt directly. Changes are saved when you click "Apply & Save"';
// Load system prompt
editor.value = window.currentSystemPrompt || 'Loading...';
editor.readOnly = false;
editor.style.opacity = '1';
} else {
systemBtn.style.background = 'transparent';
systemBtn.style.color = 'var(--text-muted)';
userBtn.style.background = 'var(--brand-primary)';
userBtn.style.color = 'white';
title.textContent = 'User Prompt (Trading Context)';
description.textContent = 'This shows the live market data, positions, and context sent to Claude with each decision';
tip.textContent = '📊 This is a live preview showing what Claude sees during trading. Read-only.';
// Load sample user prompt
editor.value = 'Loading sample context...';
editor.readOnly = true;
editor.style.opacity = '0.8';
// Fetch sample user prompt
try {
const response = await fetch('/api/prompt_presets/sample_user_prompt');
const data = await response.json();
if (data.user_prompt) {
editor.value = data.user_prompt;
} else {
editor.value = 'Error loading user prompt preview';
}
} catch (error) {
console.error('[Strategy] Error loading user prompt:', error);
editor.value = 'Error loading user prompt preview: ' + error.message;
}
}
}
// Make it globally accessible
window.switchPromptView = switchPromptView;
async function loadStrategyTab() {
// Only run if we're on the Strategy page (check if elements exist)
const select = document.getElementById('strategy-preset-select');
if (!select) {
console.log('[Strategy] Not on strategy page, skipping...');
return;
}
try {
const response = await fetch('/api/prompt_presets');
const data = await response.json();
if (data.error) {
console.error('[Strategy] Error loading presets:', data.error);
return;
}
const activeIndicator = document.getElementById('active-preset-indicator');
const activePresetNameEl = document.getElementById('active-preset-name');
// Save active preset
activePresetName = data.active_preset;
// Populate dropdown
select.innerHTML = '';
data.presets.forEach(preset => {
const option = document.createElement('option');
option.value = preset.key;
option.textContent = preset.name;
select.appendChild(option);
});
// Show active preset indicator
if (activeIndicator) activeIndicator.style.display = 'block';
if (activePresetNameEl) activePresetNameEl.textContent = data.presets.find(p => p.key === activePresetName)?.name || activePresetName;
// Load the active preset into the editor by default
await loadPresetPrompt(activePresetName);
console.log('[Strategy] Loaded presets, active:', activePresetName);
} catch (error) {
console.error('[Strategy] Error loading presets:', error);
}
}
async function loadPresetPrompt(presetKey) {
const editor = document.getElementById('prompt-editor');
if (!editor) {
console.log('[Strategy] Prompt editor not found, skipping preset load');
return;
}
try {
console.log('[Strategy] Loading preset:', presetKey);
const response = await fetch(`/api/prompt_presets/preview/${presetKey}`);
const data = await response.json();
console.log('[Strategy] Preset data received:', data);
if (data.error) {
console.error('[Strategy] API error:', data.error);
editor.value = 'Error: ' + data.error;
return;
}
currentPresetData = data;
// Update description box
const descBox = document.getElementById('preset-description-box');
const descText = document.getElementById('preset-description-text');
if (descBox && descText) {
const presetsResponse = await fetch('/api/prompt_presets');
const presetsData = await presetsResponse.json();
const preset = presetsData.presets.find(p => p.key === presetKey);
if (preset) {
descText.textContent = preset.description;
descBox.style.display = 'block';
} else {
descBox.style.display = 'none';
}
}
// Load into editor
editor.value = data.system_prompt || 'No prompt text available';
// Store for toggling
window.currentSystemPrompt = data.system_prompt;
console.log('[Strategy] Loaded preset prompt:', presetKey, '- length:', data.system_prompt?.length);
} catch (error) {
console.error('[Strategy] Error loading preset prompt:', error);
if (editor) {
editor.value = 'Error loading prompt: ' + error.message;
}
}
}
async function applyStrategy() {
const presetKey = document.getElementById('strategy-preset-select').value;
const editor = document.getElementById('prompt-editor');
const currentPromptText = editor.value;
if (!confirm(`Apply and save this strategy?\n\nThis will set "${presetKey}" as the active preset and save any prompt edits.\n\nNote: Bot must be restarted for changes to take effect.`)) {
return;
}
try {
// First, save the edited prompt text if in system view
if (currentPromptView === 'system' && currentPromptText.trim().length > 0) {
console.log('[Strategy] Saving edited system prompt...');
const promptResponse = await fetch('/api/prompts', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt_type: presetKey,
prompt_text: currentPromptText
})
});
const promptData = await promptResponse.json();
if (!promptData.success) {
alert('Failed to save prompt edits: ' + (promptData.error || 'Unknown error'));
return;
}
console.log('[Strategy] Prompt saved successfully');
}
// Then, set this as the active preset
const response = await fetch('/api/prompt_presets/active', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preset_name: presetKey })
});
const data = await response.json();
if (data.success) {
alert(`✓ Strategy applied and saved!\n\nActive preset: ${presetKey}\n\n⚠️ Restart the bot for changes to take effect.`);
await loadStrategyTab(); // Reload to update active indicator
} else {
alert('Failed to apply strategy: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('[Strategy] Error applying strategy:', error);
alert('Error applying strategy');
}
}
// Setup Strategy tab event listeners
const strategyPresetSelect = document.getElementById('strategy-preset-select');
if (strategyPresetSelect) {
strategyPresetSelect.addEventListener('change', function() {
// Just update the description, don't auto-apply
const descBox = document.getElementById('preset-description-box');
descBox.style.display = 'none';
});
}
const loadPresetBtn = document.getElementById('load-preset-btn');
if (loadPresetBtn) {
loadPresetBtn.addEventListener('click', function() {
const presetKey = document.getElementById('strategy-preset-select').value;
if (presetKey) {
loadPresetPrompt(presetKey);
} else {
alert('Please select a preset first');
}
});
}
const applyStrategyBtn = document.getElementById('apply-strategy-btn');
if (applyStrategyBtn) {
applyStrategyBtn.addEventListener('click', applyStrategy);
}
// Load strategy tab data when page loads
loadStrategyTab();
// --- Settings Tab Logic ---
async function loadSettings() {
// Only run if we're on the Settings page (check if elements exist)
const minMarginEl = document.getElementById('setting-min-margin');
if (!minMarginEl) {
console.log('[Settings] Not on settings page, skipping...');
return;
}
try {
const response = await fetch('/api/bot_config');
const data = await response.json();
if (data.success && data.config) {
document.getElementById('setting-min-margin').value = data.config.min_margin_usd;
document.getElementById('setting-max-margin').value = data.config.max_margin_usd;
document.getElementById('setting-min-balance').value = data.config.min_balance_threshold;
document.getElementById('setting-interval').value = data.config.execution_interval_seconds;
document.getElementById('setting-max-positions').value = data.config.max_open_positions;
console.log('[Settings] Loaded successfully:', data.config);
}
} catch (error) {
console.error('[Settings] Error loading config:', error);
}
}
async function saveSettings() {
const config = {
min_margin_usd: parseFloat(document.getElementById('setting-min-margin').value),
max_margin_usd: parseFloat(document.getElementById('setting-max-margin').value),
min_balance_threshold: parseFloat(document.getElementById('setting-min-balance').value),
execution_interval_seconds: parseInt(document.getElementById('setting-interval').value),
max_open_positions: parseInt(document.getElementById('setting-max-positions').value)
};
try {
const response = await fetch('/api/bot_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
alert('✓ Settings saved!\n\nChanges will take effect on the next bot cycle.');
await loadSettings(); // Reload to confirm
} else {
alert('Failed to save settings: ' + (data.error || 'Unknown error'));
}
} catch (error) {
console.error('[Settings] Error saving:', error);
alert('Error saving settings');
}
}
async function resetSettings() {
if (!confirm('Reset all settings to defaults?')) return;
const defaults = {
min_margin_usd: 1.0,
max_margin_usd: 1000.0,
min_balance_threshold: 1.0,
execution_interval_seconds: 600,
max_open_positions: 3
};
try {
const response = await fetch('/api/bot_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(defaults)
});
const data = await response.json();
if (data.success) {
alert('✓ Settings reset to defaults!');
await loadSettings();
} else {
alert('Failed to reset settings');
}
} catch (error) {
console.error('[Settings] Error resetting:', error);
alert('Error resetting settings');
}
}
// Settings tab event listeners
const saveSettingsBtn = document.getElementById('save-settings-btn');
if (saveSettingsBtn) {
saveSettingsBtn.addEventListener('click', saveSettings);
}
const resetSettingsBtn = document.getElementById('reset-settings-btn');
if (resetSettingsBtn) {
resetSettingsBtn.addEventListener('click', resetSettings);
}
// Load settings when page loads
loadSettings();
// --- Supervisor Input Logic ---
async function loadActiveInput() {
try {
const response = await fetch('/api/user_input');
const data = await response.json();
const display = document.getElementById('active-input-display');
const text = document.getElementById('active-input-text');
const time = document.getElementById('active-input-time');
const clearBtn = document.getElementById('clear-input-btn');
const indicator = document.getElementById('input-status-indicator');
// Also update the Latest Analysis section display
const analysisDisplay = document.getElementById('supervisor-input-display-content');
if (data.message) {
// Update sidebar display
display.style.display = 'block';
text.textContent = data.message;
time.textContent = 'Set: ' + new Date(data.timestamp).toLocaleString();
clearBtn.style.display = 'inline-block';
if (indicator) {
indicator.style.display = 'inline-block';
indicator.textContent = 'Active Guidance';
indicator.className = 'status-indicator running'; // Green
}
// Update Latest Analysis display
if (analysisDisplay) {
analysisDisplay.innerHTML = `
<div style="color: var(--text-primary); font-size: 0.9em; line-height: 1.5; margin-bottom: 8px;">${data.message}</div>
<div style="font-size: 0.75em; color: var(--text-muted);">Set: ${new Date(data.timestamp).toLocaleString()}</div>
`;
}
} else {
// No active input
display.style.display = 'none';
clearBtn.style.display = 'none';
if (indicator) {
indicator.style.display = 'none';
}
// Update Latest Analysis display
if (analysisDisplay) {
analysisDisplay.innerHTML = '<div style="color: var(--text-secondary); font-style: italic;">No active supervisor input</div>';
}
}
} catch (error) {
console.error('Error loading active input:', error);
}
}
// Global variable for uploaded image
let uploadedImagePath = null;
function showBotResponse(response, userQuestion) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
`;
// Create modal content
const modal = document.createElement('div');
modal.style.cssText = `
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 8px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
padding: 24px;
box-shadow: var(--shadow-lg);
`;
modal.innerHTML = `
<div style="display: flex; align-items: start; justify-content: space-between; margin-bottom: 16px;">
<h3 style="margin: 0; color: var(--text-primary); font-size: 1.1em;">💬 Bot Response</h3>
<button onclick="this.closest('[style*=fixed]').remove()" style="background: none; border: none; color: var(--text-muted); font-size: 1.5em; cursor: pointer; padding: 0; line-height: 1;">×</button>
</div>
<div style="background: rgba(255,255,255,0.03); padding: 12px; border-radius: 6px; border-left: 3px solid var(--brand-primary); margin-bottom: 16px;">
<div style="font-size: 0.75em; color: var(--text-muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px;">Your Question</div>
<div style="color: var(--text-primary); font-size: 0.9em; font-style: italic;">${userQuestion}</div>
</div>
<div style="background: rgba(40,167,69,0.1); padding: 16px; border-radius: 6px; border-left: 3px solid #28a745;">
<div style="font-size: 0.75em; color: #28a745; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">Bot's Answer</div>
<div style="color: var(--text-primary); line-height: 1.6; white-space: pre-wrap; font-size: 0.95em;">${response}</div>
</div>
<div style="margin-top: 20px; text-align: right;">
<button onclick="this.closest('[style*=fixed]').remove()" style="padding: 8px 20px; background: var(--brand-primary); border: none; border-radius: 6px; color: white; cursor: pointer; font-weight: 500;">Close</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
// Close on Escape key
const escHandler = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
function handleImageSelect(event) {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
// Show uploading state
const uploadArea = document.getElementById('image-upload-area');
const filenameEl = document.getElementById('image-filename');
uploadArea.style.display = 'block';
filenameEl.textContent = 'Uploading...';
fetch('/api/upload_image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
uploadedImagePath = data.path;
filenameEl.textContent = data.filename;
console.log('[Image Upload] Uploaded:', data.filename);
} else {
alert('Upload failed: ' + (data.error || 'Unknown error'));
removeImage();
}
})
.catch(error => {
console.error('[Image Upload] Error:', error);
alert('Upload failed');
removeImage();
});
}
function removeImage() {
uploadedImagePath = null;
document.getElementById('image-upload-area').style.display = 'none';
document.getElementById('image-input').value = '';
}
async function sendInput() {
const textarea = document.getElementById('supervisor-message');
const sendBtn = document.getElementById('send-btn');
const modeToggle = document.getElementById('mode-toggle');
const message = textarea.value.trim();
const messageType = modeToggle.checked ? 'interrupt' : 'cycle';
if (!message && !uploadedImagePath) {
alert('Please enter a message or attach an image before sending');
return;
}
// Show loading state
const originalText = sendBtn.textContent;
sendBtn.disabled = true;
sendBtn.textContent = 'Sending...';
try {
console.log(`[User Input] Sending ${messageType} message:`, message);
const payload = {
message: message || '(See attached chart)',
message_type: messageType
};
if (uploadedImagePath) {
payload.image_path = uploadedImagePath;
}
const response = await fetch('/api/user_input', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
console.log('[User Input] Response:', data);
if (data.success) {
textarea.value = '';
removeImage();
// If direct query, show bot's response
if (messageType === 'interrupt' && data.response) {
// Show response in a modal or alert
showBotResponse(data.response, message);
} else {
// Cycle mode - just update active input
loadActiveInput();
}
console.log(`[User Input] ${messageType} message sent successfully!`);
// Show success feedback
sendBtn.textContent = 'Sent ✓';
setTimeout(() => {
sendBtn.textContent = originalText;
sendBtn.disabled = false;
}, 2000);
} else {
alert('Failed to send input: ' + (data.error || 'Unknown error'));
sendBtn.textContent = originalText;
sendBtn.disabled = false;
}
} catch (error) {
console.error('[User Input] Error sending input:', error);
alert('Error sending input: ' + error.message);
sendBtn.textContent = originalText;
sendBtn.disabled = false;
}
}
async function clearInput() {
if (!confirm('Clear the active guidance?')) return;
try {
const response = await fetch('/api/user_input', { method: 'DELETE' });
const data = await response.json();
if (data.success) {
loadActiveInput();
}
} catch (error) {
console.error('Error clearing input:', error);
}
}
// Setup user input event listeners
console.log('[Init] Setting up user input listeners...');
const sendBtn = document.getElementById('send-btn');
const clearBtn = document.getElementById('clear-input-btn');
const textarea = document.getElementById('supervisor-message');
const modeToggle = document.getElementById('mode-toggle');
const modeLabel = document.getElementById('mode-label');
console.log('[Init] Send button:', sendBtn);
console.log('[Init] Clear button:', clearBtn);
console.log('[Init] Textarea:', textarea);
console.log('[Init] Mode toggle:', modeToggle);
// Update label when toggle changes
if (modeToggle && modeLabel) {
modeToggle.addEventListener('change', function() {
if (this.checked) {
modeLabel.textContent = 'Direct';
modeLabel.style.color = '#dc3545';
} else {
modeLabel.textContent = 'Cycle';
modeLabel.style.color = 'var(--text-muted)';
}
});
}
if (sendBtn) {
sendBtn.addEventListener('click', function() {
console.log('[Click] Send button clicked!');
sendInput();
});
console.log('[Init] Send button listener attached');
} else {
console.error('[Init] Send button not found!');
}
if (clearBtn) {
clearBtn.addEventListener('click', function() {
console.log('[Click] Clear button clicked!');
clearInput();
});
console.log('[Init] Clear button listener attached');
} else {
console.error('[Init] Clear button not found!');
}
// Add Enter key support (Ctrl+Enter or Cmd+Enter to send)
if (textarea) {
textarea.addEventListener('keydown', function(e) {
console.log('[Keydown] Key pressed:', e.key, 'Ctrl:', e.ctrlKey, 'Meta:', e.metaKey);
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
console.log('[Keydown] Ctrl+Enter detected, sending...');
e.preventDefault();
sendInput();
}
});
console.log('[Init] Keyboard listener attached');
} else {
console.error('[Init] Textarea not found!');
}
// Load input on start and refresh
console.log('[Init] Loading active input...');
loadActiveInput();
setInterval(loadActiveInput, 10000); // Check every 10s
</script>