trading-platform/apps/backend/test-websocket.html

507 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OrbiQuant WebSocket Test</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.controls {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.status {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-bottom: 15px;
}
.status.connected {
background: #10b981;
color: white;
}
.status.disconnected {
background: #ef4444;
color: white;
}
.status.connecting {
background: #f59e0b;
color: white;
}
button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
margin-bottom: 10px;
font-size: 14px;
transition: background 0.3s;
}
button:hover {
background: #5568d3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.subscription-panel {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.subscription-panel h2 {
margin-bottom: 15px;
color: #667eea;
}
.channel-group {
margin-bottom: 15px;
}
.channel-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"], select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 10px;
}
.output-panel {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.output-panel h2 {
margin-bottom: 15px;
color: #667eea;
}
#output {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 5px;
max-height: 500px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
}
.message {
margin-bottom: 10px;
padding: 8px;
border-left: 3px solid #667eea;
background: rgba(102, 126, 234, 0.1);
}
.message.price { border-left-color: #10b981; background: rgba(16, 185, 129, 0.1); }
.message.ticker { border-left-color: #3b82f6; background: rgba(59, 130, 246, 0.1); }
.message.kline { border-left-color: #f59e0b; background: rgba(245, 158, 11, 0.1); }
.message.error { border-left-color: #ef4444; background: rgba(239, 68, 68, 0.1); color: #fee2e2; }
.message.info { border-left-color: #8b5cf6; background: rgba(139, 92, 246, 0.1); }
.timestamp {
color: #888;
font-size: 11px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>OrbiQuant WebSocket Test Dashboard</h1>
<div class="controls">
<div id="status" class="status disconnected">Disconnected</div>
<div id="clientInfo" style="margin: 10px 0; color: #666;"></div>
<div>
<button id="connectBtn" onclick="connect()">Connect</button>
<button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
<button onclick="clearOutput()">Clear Output</button>
<button onclick="sendPing()">Send Ping</button>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="msgCount">0</div>
<div class="stat-label">Messages Received</div>
</div>
<div class="stat-card">
<div class="stat-value" id="subCount">0</div>
<div class="stat-label">Active Subscriptions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="uptime">0s</div>
<div class="stat-label">Connection Time</div>
</div>
</div>
</div>
<div class="subscription-panel">
<h2>Subscribe to Channels</h2>
<div class="channel-group">
<label>Price Updates:</label>
<input type="text" id="priceSymbol" placeholder="Symbol (e.g., BTCUSDT)" value="BTCUSDT">
<button onclick="subscribeTo('price')">Subscribe to Price</button>
</div>
<div class="channel-group">
<label>Ticker Updates:</label>
<input type="text" id="tickerSymbol" placeholder="Symbol (e.g., ETHUSDT)" value="ETHUSDT">
<button onclick="subscribeTo('ticker')">Subscribe to Ticker</button>
</div>
<div class="channel-group">
<label>Klines/Candlesticks:</label>
<input type="text" id="klineSymbol" placeholder="Symbol (e.g., BTCUSDT)" value="BTCUSDT">
<select id="klineInterval">
<option value="1m">1 minute</option>
<option value="3m">3 minutes</option>
<option value="5m" selected>5 minutes</option>
<option value="15m">15 minutes</option>
<option value="30m">30 minutes</option>
<option value="1h">1 hour</option>
<option value="4h">4 hours</option>
<option value="1d">1 day</option>
</select>
<button onclick="subscribeTo('klines')">Subscribe to Klines</button>
</div>
<div class="channel-group">
<label>Quick Subscribe:</label>
<button onclick="subscribeAll()">Subscribe to All (BTC, ETH, SOL)</button>
<button onclick="unsubscribeAll()">Unsubscribe All</button>
</div>
</div>
<div class="output-panel">
<h2>WebSocket Messages</h2>
<div id="output"></div>
</div>
</div>
<script>
let ws = null;
let messageCount = 0;
let subscriptions = new Set();
let startTime = null;
let uptimeInterval = null;
function updateStatus(status, text) {
const statusEl = document.getElementById('status');
statusEl.className = `status ${status}`;
statusEl.textContent = text;
}
function updateStats() {
document.getElementById('msgCount').textContent = messageCount;
document.getElementById('subCount').textContent = subscriptions.size;
}
function updateUptime() {
if (startTime) {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
document.getElementById('uptime').textContent = `${elapsed}s`;
}
}
function connect() {
if (ws && ws.readyState === WebSocket.OPEN) {
addOutput('Already connected', 'info');
return;
}
updateStatus('connecting', 'Connecting...');
ws = new WebSocket('ws://localhost:3000/ws');
ws.onopen = () => {
updateStatus('connected', 'Connected');
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
addOutput('✅ Connected to WebSocket server', 'info');
startTime = Date.now();
uptimeInterval = setInterval(updateUptime, 1000);
};
ws.onmessage = (event) => {
messageCount++;
updateStats();
const msg = JSON.parse(event.data);
handleMessage(msg);
};
ws.onerror = (error) => {
addOutput(`❌ WebSocket Error: ${error.message || 'Connection failed'}`, 'error');
addOutput('Make sure backend is running: npm run dev', 'error');
};
ws.onclose = () => {
updateStatus('disconnected', 'Disconnected');
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
addOutput('👋 Disconnected from server', 'info');
if (uptimeInterval) {
clearInterval(uptimeInterval);
uptimeInterval = null;
}
startTime = null;
};
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
subscriptions.clear();
updateStats();
}
}
function handleMessage(msg) {
switch (msg.type) {
case 'connected':
addOutput(`🔌 Server Info: Client ID: ${msg.data.clientId}, Auth: ${msg.data.authenticated}`, 'info');
document.getElementById('clientInfo').textContent = `Client ID: ${msg.data.clientId}`;
break;
case 'subscribed':
addOutput(`✅ Subscribed to: ${msg.channel}`, 'info');
subscriptions.add(msg.channel);
updateStats();
break;
case 'unsubscribed':
addOutput(`❌ Unsubscribed from: ${msg.channel}`, 'info');
subscriptions.delete(msg.channel);
updateStats();
break;
case 'price':
const p = msg.data;
const changeIcon = p.changePercent24h >= 0 ? '📈' : '📉';
addOutput(
`${changeIcon} ${p.symbol}: $${p.price.toLocaleString()} (${p.changePercent24h >= 0 ? '+' : ''}${p.changePercent24h.toFixed(2)}%) Vol: ${p.volume24h.toLocaleString()}`,
'price'
);
break;
case 'ticker':
const t = msg.data;
addOutput(
`📊 ${t.symbol}: $${t.price.toLocaleString()} | Bid/Ask: $${t.bid}/$${t.ask} | 24h: ${t.changePercent >= 0 ? '+' : ''}${t.changePercent.toFixed(2)}%`,
'ticker'
);
break;
case 'kline':
const k = msg.data;
addOutput(
`📈 ${k.symbol} ${k.interval}: O:$${k.open} H:$${k.high} L:$${k.low} C:$${k.close} V:${k.volume.toFixed(2)} ${k.isFinal ? '✓' : '⏳'}`,
'kline'
);
break;
case 'trade':
const tr = msg.data;
addOutput(
`💸 ${tr.symbol}: ${tr.side.toUpperCase()} ${tr.quantity} @ $${tr.price}`,
'info'
);
break;
case 'pong':
addOutput('🏓 Pong received', 'info');
break;
case 'error':
addOutput(`❌ Error: ${msg.data.message}`, 'error');
break;
default:
addOutput(`📨 ${msg.type}: ${JSON.stringify(msg.data || {})}`, 'info');
}
}
function addOutput(text, type = 'info') {
const output = document.getElementById('output');
const msg = document.createElement('div');
msg.className = `message ${type}`;
const timestamp = new Date().toLocaleTimeString();
msg.innerHTML = `<span class="timestamp">[${timestamp}]</span> ${text}`;
output.appendChild(msg);
output.scrollTop = output.scrollHeight;
// Keep only last 100 messages
while (output.children.length > 100) {
output.removeChild(output.firstChild);
}
}
function clearOutput() {
document.getElementById('output').innerHTML = '';
messageCount = 0;
updateStats();
}
function subscribeTo(type) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('Please connect first!');
return;
}
let channel;
switch (type) {
case 'price':
const priceSymbol = document.getElementById('priceSymbol').value.toUpperCase();
channel = `price:${priceSymbol}`;
break;
case 'ticker':
const tickerSymbol = document.getElementById('tickerSymbol').value.toUpperCase();
channel = `ticker:${tickerSymbol}`;
break;
case 'klines':
const klineSymbol = document.getElementById('klineSymbol').value.toUpperCase();
const interval = document.getElementById('klineInterval').value;
channel = `klines:${klineSymbol}:${interval}`;
break;
}
ws.send(JSON.stringify({
type: 'subscribe',
channels: [channel]
}));
}
function subscribeAll() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('Please connect first!');
return;
}
const channels = [
'price:BTCUSDT',
'price:ETHUSDT',
'price:SOLUSDT',
'ticker:BTCUSDT',
'klines:BTCUSDT:1m',
'klines:ETHUSDT:5m'
];
ws.send(JSON.stringify({
type: 'subscribe',
channels: channels
}));
}
function unsubscribeAll() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('Please connect first!');
return;
}
ws.send(JSON.stringify({
type: 'unsubscribe',
channels: Array.from(subscriptions)
}));
}
function sendPing() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('Please connect first!');
return;
}
ws.send(JSON.stringify({ type: 'ping' }));
addOutput('🏓 Ping sent', 'info');
}
// Auto-connect on page load
window.addEventListener('load', () => {
addOutput('👋 Welcome to OrbiQuant WebSocket Test Dashboard', 'info');
addOutput('Click "Connect" to start, then subscribe to channels', 'info');
});
</script>
</body>
</html>