Skip to content

Instantly share code, notes, and snippets.

@raress96
Created September 25, 2025 11:17
Show Gist options
  • Select an option

  • Save raress96/6345010f720964a993def0f56cda2087 to your computer and use it in GitHub Desktop.

Select an option

Save raress96/6345010f720964a993def0f56cda2087 to your computer and use it in GitHub Desktop.
How to Build a Trading Dashboard with Surflux
<!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>
<!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>
{
"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