507 lines
14 KiB
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>
|