Created
September 25, 2025 11:17
-
-
Save raress96/6345010f720964a993def0f56cda2087 to your computer and use it in GitHub Desktop.
How to Build a Trading Dashboard with Surflux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Surflux DeepBook Trading Dashboard</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lightweight-charts.standalone.production.js"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| 'dark': '#0B0E11', | |
| 'dark-light': '#13171D', | |
| 'gray-border': '#1E2329', | |
| 'gray-lighter': '#2B3139', | |
| 'text-primary': '#EAECEF', | |
| 'text-secondary': '#848E9C', | |
| 'green': '#0ECB81', | |
| 'red': '#F6465D', | |
| 'yellow': '#F0B90B', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body class="bg-dark text-text-primary font-sans h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-dark-light border-b border-gray-border px-4 lg:px-6 py-3"> | |
| <!-- Top row on mobile, single row on desktop --> | |
| <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3"> | |
| <!-- Left section --> | |
| <div class="flex flex-col sm:flex-row sm:items-center gap-3 lg:gap-5"> | |
| <div class="flex items-center space-x-3"> | |
| <a href="index.html" class="text-text-secondary hover:text-text-primary text-sm"> | |
| ← SSE Demo | |
| </a> | |
| <div class="w-px h-5 bg-gray-border"></div> | |
| </div> | |
| <h1 class="text-yellow text-xl font-bold">Surflux DeepBook</h1> | |
| <div class="bg-gray-border rounded px-3 py-1.5 w-fit"> | |
| <input type="text" id="poolName" value="SUI_USDC" placeholder="Pool" | |
| class="bg-transparent text-text-primary text-sm font-semibold w-28 focus:outline-none"> | |
| </div> | |
| <div class="flex items-center space-x-3 lg:space-x-5"> | |
| <div id="currentPrice" class="text-lg lg:text-xl font-semibold text-green">--</div> | |
| <div id="priceChange" class="text-sm px-2 py-1 rounded bg-green/10 text-green">--</div> | |
| </div> | |
| </div> | |
| <!-- Right section --> | |
| <div class="flex flex-col sm:flex-row sm:items-center gap-3"> | |
| <input type="text" id="apiKey" placeholder="API Key" value="test_api_key" | |
| class="bg-gray-border border border-gray-lighter text-text-primary px-3 py-2 rounded text-sm w-full sm:w-44 focus:outline-none"> | |
| <div class="flex items-center space-x-3"> | |
| <div id="statusIndicator" class="w-2 h-2 rounded-full bg-text-secondary"></div> | |
| <button id="connectBtn" onclick="connect()" | |
| class="bg-yellow text-dark px-4 py-2 rounded font-semibold text-sm hover:bg-yellow/90 disabled:bg-gray-lighter disabled:text-text-secondary w-full sm:w-auto"> | |
| Connect | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-1 grid lg:grid-cols-[1fr_320px_300px] lg:grid-rows-[500px_1fr] grid-cols-1 grid-rows-none gap-px bg-gray-border overflow-hidden pb-4"> | |
| <!-- Price Chart --> | |
| <section class="bg-dark-light lg:col-span-1 lg:row-span-1 h-96 lg:h-auto w-full"> | |
| <div class="px-4 py-3 border-b border-gray-border flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2"> | |
| <h2 class="text-sm font-semibold">Price Chart</h2> | |
| <div class="flex flex-wrap gap-1 sm:space-x-2"> | |
| <button class="timeframe-btn active" data-timeframe="1m">1m</button> | |
| <button class="timeframe-btn" data-timeframe="5m">5m</button> | |
| <button class="timeframe-btn" data-timeframe="15m">15m</button> | |
| <button class="timeframe-btn" data-timeframe="1h">1h</button> | |
| <button class="timeframe-btn" data-timeframe="4h">4h</button> | |
| <button class="timeframe-btn" data-timeframe="1d">1d</button> | |
| </div> | |
| </div> | |
| <div id="chart" class="w-full h-full"></div> | |
| </section> | |
| <!-- Order Book --> | |
| <section class="bg-dark-light lg:col-span-1 lg:row-span-2 h-96 lg:h-auto"> | |
| <div class="px-4 py-3 border-b border-gray-border"> | |
| <h2 class="text-sm font-semibold">Order Book</h2> | |
| </div> | |
| <div class="h-full overflow-y-auto scrollbar"> | |
| <div id="asksList" class="pb-2"></div> | |
| <div id="spreadRow" class="bg-gray-border px-3 py-2 text-center text-yellow text-sm font-semibold">--</div> | |
| <div id="bidsList" class="pt-2"></div> | |
| </div> | |
| </section> | |
| <!-- Recent Trades --> | |
| <section class="bg-dark-light lg:col-span-1 lg:row-span-2 h-96 lg:h-auto"> | |
| <div class="px-4 py-3 border-b border-gray-border"> | |
| <h2 class="text-sm font-semibold">Recent Trades</h2> | |
| </div> | |
| <div class="h-full overflow-y-auto scrollbar"> | |
| <div class="grid grid-cols-3 gap-2 px-2 py-2 text-text-secondary text-xs uppercase border-b border-gray-border"> | |
| <div>Price</div> | |
| <div class="text-right">Amount</div> | |
| <div class="text-right">Time</div> | |
| </div> | |
| <div id="tradesList"></div> | |
| </div> | |
| </section> | |
| <!-- Market Depth --> | |
| <section class="bg-dark-light lg:col-span-1 lg:row-span-1 h-96 lg:h-auto"> | |
| <div class="px-4 py-3 border-b border-gray-border"> | |
| <h2 class="text-sm font-semibold">Market Depth</h2> | |
| </div> | |
| <div id="depthChart" class="flex flex-col h-full p-4"> | |
| <div class="flex justify-between text-xs text-text-secondary mb-2"> | |
| <span>Bids</span> | |
| <span>Asks</span> | |
| </div> | |
| <div class="flex-1 flex"> | |
| <!-- Y-axis labels (amounts) --> | |
| <div id="yAxisLabels" class="flex flex-col justify-between text-xs text-text-secondary pr-2 py-1"> | |
| <!-- Amount labels will be inserted here --> | |
| </div> | |
| <!-- Chart area --> | |
| <div class="flex-1 flex flex-col"> | |
| <div id="depthBars" class="flex-1 flex items-end gap-px"> | |
| <!-- Depth bars will be inserted here --> | |
| </div> | |
| <!-- X-axis labels (prices) --> | |
| <div id="xAxisLabels" class="flex justify-between text-xs text-text-secondary mt-1 px-1"> | |
| <!-- Price labels will be inserted here --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between text-xs text-text-secondary mt-2"> | |
| <span>Total: <span id="bidTotal">0</span></span> | |
| <span>Total: <span id="askTotal">0</span></span> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Error Toast --> | |
| <div id="errorMessage" class="fixed top-20 right-5 bg-red text-white px-4 py-3 rounded text-sm z-50 hidden"></div> | |
| <style> | |
| .scrollbar::-webkit-scrollbar { width: 4px; } | |
| .scrollbar::-webkit-scrollbar-track { background: #13171D; } | |
| .scrollbar::-webkit-scrollbar-thumb { background: #2B3139; border-radius: 2px; } | |
| .scrollbar::-webkit-scrollbar-thumb:hover { background: #3B4149; } | |
| .timeframe-btn { | |
| @apply px-3 py-1 bg-transparent text-text-secondary text-sm rounded cursor-pointer hover:bg-gray-border; | |
| } | |
| .timeframe-btn.active { | |
| @apply bg-gray-border text-yellow; | |
| } | |
| </style> | |
| <script> | |
| let chart, candlestickSeries; | |
| let eventSource = null; | |
| let currentTimeframe = '1m'; | |
| let poolData = null; | |
| let lastPrice = 0; | |
| let baseDecimals = 9, quoteDecimals = 6; | |
| const API_URL = 'http://localhost:8000'; | |
| // Initialize charts | |
| function initCharts() { | |
| const chartOptions = { | |
| layout: { background: { color: '#13171D' }, textColor: '#848E9C' }, | |
| grid: { vertLines: { color: '#1E2329' }, horzLines: { color: '#1E2329' } }, | |
| timeScale: { timeVisible: true, borderColor: '#1E2329' }, | |
| rightPriceScale: { borderColor: '#1E2329' }, | |
| handleScroll: { mouseWheel: true, pressedMouseMove: true } | |
| }; | |
| const chartContainer = document.getElementById('chart'); | |
| chart = LightweightCharts.createChart(chartContainer, { | |
| ...chartOptions, | |
| width: chartContainer.offsetWidth, | |
| height: chartContainer.offsetHeight || 400 | |
| }); | |
| candlestickSeries = chart.addCandlestickSeries({ | |
| upColor: '#0ECB81', | |
| downColor: '#F6465D', | |
| borderVisible: false, | |
| wickUpColor: '#0ECB81', | |
| wickDownColor: '#F6465D', | |
| }); | |
| // Resize handler | |
| window.addEventListener('resize', () => { | |
| const container = document.getElementById('chart'); | |
| chart.applyOptions({ | |
| width: container.offsetWidth, | |
| height: container.offsetHeight || 400 | |
| }); | |
| }); | |
| } | |
| // API functions | |
| async function apiRequest(endpoint, params = {}) { | |
| const url = new URL(`${API_URL}${endpoint}`); | |
| Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value)); | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`API Error: ${response.status} ${response.statusText}`); | |
| return response.json(); | |
| } | |
| async function fetchPoolInfo(apiKey, poolName) { | |
| const pools = await apiRequest('/deepbook/get_pools', { 'api-key': apiKey }); | |
| poolData = pools.find(p => p.pool_name === poolName); | |
| if (!poolData) throw new Error(`Pool ${poolName} not found`); | |
| baseDecimals = poolData.base_asset_decimals; | |
| quoteDecimals = poolData.quote_asset_decimals; | |
| } | |
| async function fetchOHLCV(apiKey, poolName, timeframe) { | |
| const to = Math.floor(Date.now() / 1000); | |
| const from = to - 24 * 60 * 60; | |
| const data = await apiRequest(`/deepbook/${poolName}/ohlcv/${timeframe}`, { | |
| 'api-key': apiKey, | |
| from, | |
| to, | |
| limit: 500 | |
| }); | |
| const candleData = data.map(candle => ({ | |
| time: Math.floor(new Date(candle.timestamp).getTime() / 1000), | |
| open: parseFloat(candle.open) / Math.pow(10, quoteDecimals), | |
| high: parseFloat(candle.high) / Math.pow(10, quoteDecimals), | |
| low: parseFloat(candle.low) / Math.pow(10, quoteDecimals), | |
| close: parseFloat(candle.close) / Math.pow(10, quoteDecimals), | |
| })).reverse(); | |
| candlestickSeries.setData(candleData); | |
| chart.timeScale().fitContent(); | |
| if (candleData.length > 0) { | |
| const latest = candleData[candleData.length - 1]; | |
| updatePrice(latest.close, candleData[0]?.close || latest.close); | |
| } | |
| } | |
| async function fetchOrderBook(apiKey, poolName) { | |
| const data = await apiRequest(`/deepbook/${poolName}/order-book-depth`, { | |
| 'api-key': apiKey, | |
| limit: 15 | |
| }); | |
| updateOrderBook(data); | |
| } | |
| async function fetchInitialTrades(apiKey, poolName) { | |
| const data = await apiRequest(`/deepbook/${poolName}/trades`, { | |
| 'api-key': apiKey | |
| }); | |
| // Clear existing trades | |
| document.getElementById('tradesList').innerHTML = ''; | |
| // Add trades in reverse order (newest first) | |
| data.reverse().forEach(trade => addTrade(trade)); | |
| } | |
| function connectLiveTrades(apiKey, poolName) { | |
| if (eventSource) eventSource.close(); | |
| const url = new URL(`${API_URL}/deepbook/${poolName}/live-trades`); | |
| url.searchParams.append('api-key', apiKey); | |
| eventSource = new EventSource(url); | |
| eventSource.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'deepbook_order_book_depth' && data.data?.bids) { | |
| updateOrderBook(data.data); | |
| } else if (data.type === 'deepbook_live_trades') { | |
| addTrade(data.data); | |
| } | |
| } catch (e) { | |
| console.error('SSE parse error:', e); | |
| } | |
| }; | |
| eventSource.onerror = () => { | |
| document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-text-secondary'; | |
| document.getElementById('connectBtn').disabled = false; | |
| }; | |
| } | |
| // UI Update functions | |
| function updateOrderBook(orderBook) { | |
| if (!orderBook.asks || !orderBook.bids) return; | |
| const maxAmount = Math.max( | |
| ...orderBook.asks.map(a => parseFloat(a.total_quantity)), | |
| ...orderBook.bids.map(b => parseFloat(b.total_quantity)) | |
| ); | |
| const createOrderRow = (order, isBid) => { | |
| const price = parseFloat(order.price) / Math.pow(10, quoteDecimals); | |
| const amount = parseFloat(order.total_quantity) / Math.pow(10, baseDecimals); | |
| const total = price * amount; | |
| const depth = (parseFloat(order.total_quantity) / maxAmount) * 100; | |
| const colorClass = isBid ? 'text-green' : 'text-red'; | |
| const bgClass = isBid ? 'bg-green/10' : 'bg-red/10'; | |
| return ` | |
| <div class="relative grid grid-cols-3 gap-2 px-2 py-1 text-xs font-mono"> | |
| <div class="absolute inset-y-0 right-0 ${bgClass} opacity-60" style="width: ${depth}%"></div> | |
| <div class="relative ${colorClass}">${price.toFixed(4)}</div> | |
| <div class="relative text-right">${amount.toFixed(2)}</div> | |
| <div class="relative text-right">${total.toFixed(2)}</div> | |
| </div> | |
| `; | |
| }; | |
| // Header | |
| const headerHtml = ` | |
| <div class="grid grid-cols-3 gap-2 px-2 py-1 text-text-secondary text-xs uppercase border-b border-gray-border"> | |
| <div>Price</div> | |
| <div class="text-right">Amount</div> | |
| <div class="text-right">Total</div> | |
| </div> | |
| `; | |
| const asksHtml = headerHtml + orderBook.asks.slice(0, 15).reverse() | |
| .map(ask => createOrderRow(ask, false)).join(''); | |
| const bidsHtml = headerHtml + orderBook.bids.slice(0, 15) | |
| .map(bid => createOrderRow(bid, true)).join(''); | |
| document.getElementById('asksList').innerHTML = asksHtml; | |
| document.getElementById('bidsList').innerHTML = bidsHtml; | |
| // Update spread | |
| if (orderBook.asks.length > 0 && orderBook.bids.length > 0) { | |
| const bestAsk = parseFloat(orderBook.asks[0].price) / Math.pow(10, quoteDecimals); | |
| const bestBid = parseFloat(orderBook.bids[0].price) / Math.pow(10, quoteDecimals); | |
| const spread = bestAsk - bestBid; | |
| const spreadPercent = (spread / bestAsk) * 100; | |
| document.getElementById('spreadRow').textContent = `${spread.toFixed(4)} (${spreadPercent.toFixed(2)}%)`; | |
| // Update current price with mid price | |
| const midPrice = (bestAsk + bestBid) / 2; | |
| if (!lastPrice) updatePrice(midPrice, midPrice); | |
| } | |
| updateDepthChart(orderBook); | |
| } | |
| function updateDepthChart(orderBook) { | |
| if (!orderBook.bids || !orderBook.asks || orderBook.bids.length === 0 || orderBook.asks.length === 0) { | |
| return; | |
| } | |
| const barsContainer = document.getElementById('depthBars'); | |
| const xAxisLabels = document.getElementById('xAxisLabels'); | |
| const yAxisLabels = document.getElementById('yAxisLabels'); | |
| barsContainer.innerHTML = ''; | |
| xAxisLabels.innerHTML = ''; | |
| yAxisLabels.innerHTML = ''; | |
| // Process bids and asks (take top 10 levels for better readability) | |
| const maxLevels = 10; | |
| const bids = orderBook.bids.slice(0, maxLevels); | |
| const asks = orderBook.asks.slice(0, maxLevels); | |
| // Calculate cumulative amounts | |
| let bidCumulative = 0; | |
| let askCumulative = 0; | |
| const bidAmounts = bids.map(bid => { | |
| const amount = parseFloat(bid.total_quantity) / Math.pow(10, baseDecimals); | |
| bidCumulative += amount; | |
| return bidCumulative; | |
| }); | |
| const askAmounts = asks.map(ask => { | |
| const amount = parseFloat(ask.total_quantity) / Math.pow(10, baseDecimals); | |
| askCumulative += amount; | |
| return askCumulative; | |
| }); | |
| // Find max cumulative for scaling | |
| const maxAmount = Math.max( | |
| bidAmounts[bidAmounts.length - 1] || 0, | |
| askAmounts[askAmounts.length - 1] || 0 | |
| ); | |
| // Create Y-axis labels (amounts) - 5 levels | |
| for (let i = 4; i >= 0; i--) { | |
| const label = document.createElement('div'); | |
| const value = (maxAmount * (i + 1)) / 5; | |
| label.textContent = value > 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(0); | |
| label.className = 'text-right'; | |
| yAxisLabels.appendChild(label); | |
| } | |
| // Collect all prices for X-axis labels | |
| const allPrices = []; | |
| // Create bid bars (left side, reversed order) | |
| for (let i = bids.length - 1; i >= 0; i--) { | |
| const price = parseFloat(bids[i].price) / Math.pow(10, quoteDecimals); | |
| allPrices.push(price); | |
| const height = (bidAmounts[i] / maxAmount) * 100; | |
| const bar = document.createElement('div'); | |
| bar.className = 'flex-1 bg-green/40 hover:bg-green/60 transition-colors cursor-pointer'; | |
| bar.style.height = `${height}%`; | |
| bar.title = `Price: ${price.toFixed(4)}, Amount: ${bidAmounts[i].toFixed(2)}`; | |
| barsContainer.appendChild(bar); | |
| } | |
| // Create ask bars (right side) | |
| for (let i = 0; i < asks.length; i++) { | |
| const price = parseFloat(asks[i].price) / Math.pow(10, quoteDecimals); | |
| allPrices.push(price); | |
| const height = (askAmounts[i] / maxAmount) * 100; | |
| const bar = document.createElement('div'); | |
| bar.className = 'flex-1 bg-red/40 hover:bg-red/60 transition-colors cursor-pointer'; | |
| bar.style.height = `${height}%`; | |
| bar.title = `Price: ${price.toFixed(4)}, Amount: ${askAmounts[i].toFixed(2)}`; | |
| barsContainer.appendChild(bar); | |
| } | |
| // Create X-axis labels (prices) - show 5 strategic prices | |
| const minPrice = Math.min(...allPrices); | |
| const maxPrice = Math.max(...allPrices); | |
| const midPrice = (minPrice + maxPrice) / 2; | |
| const strategicPrices = [ | |
| minPrice, | |
| (minPrice + midPrice) / 2, | |
| midPrice, | |
| (midPrice + maxPrice) / 2, | |
| maxPrice | |
| ]; | |
| strategicPrices.forEach(price => { | |
| const label = document.createElement('div'); | |
| label.textContent = price.toFixed(4); | |
| label.className = 'text-center'; | |
| xAxisLabels.appendChild(label); | |
| }); | |
| // Update totals | |
| document.getElementById('bidTotal').textContent = bidCumulative.toFixed(2); | |
| document.getElementById('askTotal').textContent = askCumulative.toFixed(2); | |
| } | |
| function addTrade(trade) { | |
| const price = parseFloat(trade.price) / Math.pow(10, quoteDecimals); | |
| const amount = parseFloat(trade.base_quantity) / Math.pow(10, baseDecimals); | |
| const time = new Date(parseInt(trade.checkpoint_timestamp_ms)).toLocaleTimeString(); | |
| const isBuy = trade.taker_is_bid; | |
| const priceClass = isBuy ? 'text-green' : 'text-red'; | |
| const tradeHtml = ` | |
| <div class="grid grid-cols-3 gap-2 px-2 py-1.5 text-xs font-mono border-b border-gray-border/50 hover:bg-gray-border/30"> | |
| <div class="${priceClass}">${price.toFixed(4)}</div> | |
| <div class="text-right">${amount.toFixed(2)}</div> | |
| <div class="text-right text-text-secondary text-xs">${time}</div> | |
| </div> | |
| `; | |
| const tradesList = document.getElementById('tradesList'); | |
| tradesList.insertAdjacentHTML('afterbegin', tradeHtml); | |
| // Keep only last 50 trades | |
| while (tradesList.children.length > 50) { | |
| tradesList.removeChild(tradesList.lastChild); | |
| } | |
| updatePrice(price, lastPrice || price); | |
| lastPrice = price; | |
| } | |
| function updatePrice(current, previous) { | |
| const change = previous ? ((current - previous) / previous) * 100 : 0; | |
| const priceEl = document.getElementById('currentPrice'); | |
| const changeEl = document.getElementById('priceChange'); | |
| priceEl.textContent = current.toFixed(4); | |
| priceEl.className = `text-xl font-semibold ${current >= previous ? 'text-green' : 'text-red'}`; | |
| changeEl.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`; | |
| changeEl.className = `text-sm px-2 py-1 rounded ${ | |
| change >= 0 ? 'bg-green/10 text-green' : 'bg-red/10 text-red' | |
| }`; | |
| } | |
| function showError(message) { | |
| const errorEl = document.getElementById('errorMessage'); | |
| errorEl.textContent = message; | |
| errorEl.classList.remove('hidden'); | |
| setTimeout(() => errorEl.classList.add('hidden'), 5000); | |
| } | |
| // Event handlers | |
| async function connect() { | |
| const poolName = document.getElementById('poolName').value.trim(); | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| if (!poolName || !apiKey) { | |
| showError('Please enter pool name and API key'); | |
| return; | |
| } | |
| const btn = document.getElementById('connectBtn'); | |
| btn.disabled = true; | |
| btn.textContent = 'Connecting...'; | |
| try { | |
| await fetchPoolInfo(apiKey, poolName); | |
| await fetchOHLCV(apiKey, poolName, currentTimeframe); | |
| await fetchOrderBook(apiKey, poolName); | |
| await fetchInitialTrades(apiKey, poolName); | |
| connectLiveTrades(apiKey, poolName); | |
| document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-green'; | |
| btn.textContent = 'Connected'; | |
| } catch (error) { | |
| showError(error.message); | |
| btn.disabled = false; | |
| btn.textContent = 'Connect'; | |
| } | |
| } | |
| // Timeframe selection | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initCharts(); | |
| document.querySelectorAll('.timeframe-btn').forEach(btn => { | |
| btn.addEventListener('click', async () => { | |
| currentTimeframe = btn.dataset.timeframe; | |
| // Update active state | |
| document.querySelectorAll('.timeframe-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Fetch new data | |
| const poolName = document.getElementById('poolName').value.trim(); | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| if (poolName && apiKey && poolData) { | |
| try { | |
| await fetchOHLCV(apiKey, poolName, currentTimeframe); | |
| } catch (error) { | |
| showError('Failed to fetch OHLCV data'); | |
| } | |
| } | |
| }); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Surflux SSE Demo</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 font-mono min-h-screen p-6"> | |
| <div class="max-w-4xl mx-auto"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h1 class="text-2xl font-bold">Surflux SSE Demo</h1> | |
| <a href="deepbook.html" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm"> | |
| 📊 Trading Dashboard | |
| </a> | |
| </div> | |
| <!-- Connection Form --> | |
| <div class="bg-gray-800 p-4 rounded-lg mb-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> | |
| <input type="text" id="apiKey" placeholder="API Key" value="test_api_key" | |
| class="bg-gray-700 text-gray-100 px-3 py-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <input type="text" id="serverUrl" placeholder="Server URL" value="http://localhost:8000/events" | |
| class="bg-gray-700 text-gray-100 px-3 py-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <input type="text" id="lastId" placeholder="Last ID (defaults to '0')" | |
| class="bg-gray-700 text-gray-100 px-3 py-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div class="text-xs text-gray-400 mt-2 text-end"> | |
| last-id: <span class="font-mono">'0'</span> = consume from beginning, <span class="font-mono">'$'</span> = consume only real time | |
| </div> | |
| <div class="flex gap-3"> | |
| <button onclick="connect()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">Connect</button> | |
| <button onclick="disconnect()" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">Disconnect</button> | |
| <button onclick="clearEvents()" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded">Clear</button> | |
| </div> | |
| </div> | |
| <!-- Status --> | |
| <div class="flex items-center gap-4 mb-4"> | |
| <div class="flex items-center gap-2"> | |
| <div id="statusDot" class="w-3 h-3 rounded-full bg-gray-500"></div> | |
| <span id="status">Disconnected</span> | |
| </div> | |
| <div>Events: <span id="count" class="font-bold">0</span></div> | |
| </div> | |
| <!-- Events --> | |
| <div class="bg-gray-800 rounded-lg"> | |
| <div class="p-4 border-b border-gray-700"> | |
| <h2 class="font-semibold">Live Events</h2> | |
| </div> | |
| <div id="events" class="h-96 overflow-y-auto p-4 space-y-2 text-sm"></div> | |
| </div> | |
| </div> | |
| <script> | |
| let eventSource = null; | |
| let eventCount = 0; | |
| function setStatus(text, color = 'gray') { | |
| document.getElementById('status').textContent = text; | |
| const colors = { green: 'bg-green-500', blue: 'bg-blue-500', red: 'bg-red-500', gray: 'bg-gray-500' }; | |
| document.getElementById('statusDot').className = `w-3 h-3 rounded-full ${colors[color]}`; | |
| } | |
| function connect() { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const serverUrl = document.getElementById('serverUrl').value.trim(); | |
| const lastId = document.getElementById('lastId').value.trim(); | |
| if (!apiKey) { | |
| alert('Please enter an API key'); | |
| return; | |
| } | |
| disconnect(); | |
| clearEvents(); | |
| setStatus('Connecting...', 'blue'); | |
| let url = `${serverUrl}?api-key=${encodeURIComponent(apiKey)}`; | |
| if (lastId) { | |
| url += `&last-id=${encodeURIComponent(lastId)}`; | |
| } | |
| eventSource = new EventSource(url); | |
| eventSource.onopen = () => setStatus('Connected', 'green'); | |
| eventSource.onerror = () => setStatus('Error', 'red'); | |
| eventSource.onmessage = (event) => { | |
| eventCount++; | |
| document.getElementById('count').textContent = eventCount; | |
| const eventDiv = document.createElement('div'); | |
| eventDiv.className = 'bg-gray-700 p-3 rounded border-l-4 border-blue-500'; | |
| const timestamp = new Date().toLocaleTimeString(); | |
| eventDiv.innerHTML = ` | |
| <div class="text-blue-400 text-xs mb-1">last-id: ${event.lastEventId} - ${timestamp}</div> | |
| <pre class="whitespace-pre-wrap text-xs">${event.data}</pre> | |
| `; | |
| const container = document.getElementById('events'); | |
| container.insertBefore(eventDiv, container.firstChild); | |
| // Keep only last 20 events | |
| while (container.children.length > 20) { | |
| container.removeChild(container.lastChild); | |
| } | |
| }; | |
| } | |
| function disconnect() { | |
| if (eventSource) { | |
| eventSource.close(); | |
| eventSource = null; | |
| setStatus('Disconnected', 'gray'); | |
| } | |
| } | |
| function clearEvents() { | |
| document.getElementById('events').innerHTML = ''; | |
| eventCount = 0; | |
| document.getElementById('count').textContent = '0'; | |
| } | |
| // Focus API key input on load | |
| document.addEventListener('DOMContentLoaded', () => { | |
| document.getElementById('apiKey').focus(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "sse-demo", | |
| "version": "1.0.0", | |
| "description": "Simple demo for SSE endpoint consumption", | |
| "main": "index.html", | |
| "scripts": { | |
| "dev": "npx http-server -p 8080 -c-1 --cors" | |
| }, | |
| "devDependencies": { | |
| "http-server": "^14.1.1" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment