const recnetFetch = (input, init = {}) => fetch(input, { ...init, credentials: init.credentials ?? 'same-origin' }); const state = { snapshot: null, requests: [], chats: [], paused: false, cpuHistory: [], memoryHistory: [], stream: null }; const byId = id => document.getElementById(id); const esc = value => String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); const formatBytes = value => { const bytes = Number(value) || 0; if (bytes < 1024) return `${bytes} B`; const units = ['KB', 'MB', 'GB', 'TB']; let size = bytes / 1024, index = 0; while (size >= 1024 && index < units.length - 1) { size /= 1024; index++; } return `${size >= 100 ? size.toFixed(0) : size >= 10 ? size.toFixed(1) : size.toFixed(2)} ${units[index]}`; }; const formatRate = value => `${formatBytes(value)}/s`; const formatTime = value => new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(new Date(value)); const formatUptime = seconds => { seconds = Math.max(0, Number(seconds) || 0); const days = Math.floor(seconds / 86400), hours = Math.floor(seconds % 86400 / 3600), minutes = Math.floor(seconds % 3600 / 60); return days ? `${days}d ${hours}h` : hours ? `${hours}h ${minutes}m` : `${minutes}m`; }; function mergeBy(items, incoming, key, limit) { const map = new Map(items.map(item => [key(item), item])); incoming.forEach(item => map.set(key(item), item)); return [...map.values()].sort((a, b) => new Date(b.at || b.createdAt) - new Date(a.at || a.createdAt)).slice(0, limit); } function setStreamState(mode, label) { const el = byId('streamState'); el.className = `stream-state ${mode}`; el.innerHTML = ` ${esc(label)}`; } function renderSnapshot(data) { state.snapshot = data; if (!state.paused) state.requests = mergeBy(state.requests, data.requests || [], item => `${item.at}|${item.method}|${item.path}|${item.status}|${item.durationMs}`, 2000); state.chats = mergeBy(state.chats, data.chats || [], item => item.messageId, 2000); state.cpuHistory.push({ at: data.generatedAt, value: Number(data.server.cpuPercent) || 0 }); state.memoryHistory.push({ at: data.generatedAt, value: (Number(data.server.memoryBytes) || 0) / 1048576 }); state.cpuHistory = state.cpuHistory.slice(-120); state.memoryHistory = state.memoryHistory.slice(-120); const latest = (data.series || []).at(-1) || {}; const networkRate = (Number(latest.inboundBytes) || 0) + (Number(latest.outboundBytes) || 0); const errorRate = data.totals.requests ? data.totals.errors / data.totals.requests * 100 : 0; byId('lastUpdated').textContent = formatTime(data.generatedAt); byId('onlineMetric').textContent = Number(data.totals.onlinePlayers).toLocaleString(); byId('socketMetric').textContent = `${Number(data.totals.connectedSockets).toLocaleString()} live sockets`; byId('requestMetric').textContent = Number(data.totals.requests).toLocaleString(); byId('requestRateMetric').textContent = `${Number(latest.requests || 0).toLocaleString()} per second`; byId('networkMetric').textContent = formatBytes(Number(data.totals.inboundBytes) + Number(data.totals.outboundBytes)); byId('networkRateMetric').textContent = formatRate(networkRate); byId('resourceMetric').textContent = `${Number(data.server.cpuPercent).toFixed(1)}% / ${formatBytes(data.server.memoryBytes)}`; byId('uptimeMetric').textContent = `${formatUptime(data.server.uptimeSeconds)} uptime`; byId('errorMetric').textContent = Number(data.totals.errors).toLocaleString(); byId('errorRateMetric').textContent = `${errorRate.toFixed(2)}% of requests`; byId('chatMetric').textContent = Number(data.totals.chatMessages).toLocaleString(); byId('chatThreadMetric').textContent = `${Number(data.totals.chatThreads).toLocaleString()} threads`; renderOnline(data.onlinePlayers || []); renderRequests(); renderChats(); drawAllCharts(); } function renderOnline(players) { byId('onlineCountBadge').textContent = `${players.length} online`; byId('onlinePlayers').innerHTML = players.length ? players.map(player => `
${esc(player.displayName || player.username || `Player ${player.accountId}`)}${esc(player.room || 'Online')} · ${esc(player.device || 'Unknown')}
${Number(player.sockets) || 0} WS
`).join('') : '
Nobody is online right now.
'; } function renderRequests() { const filter = byId('requestFilter').value.trim().toLowerCase(); const rows = state.requests.filter(item => !filter || `${item.status} ${item.method} ${item.path}`.toLowerCase().includes(filter)).slice(0, 500); byId('requestRows').innerHTML = rows.length ? rows.map(item => { const statusClass = item.status >= 500 ? 'error' : item.status >= 400 ? 'warn' : ''; return `${formatTime(item.at)}${item.status}${esc(item.method)}${esc(item.path)}${Number(item.durationMs).toLocaleString()} ms${formatBytes(item.outboundBytes)}`; }).join('') : 'No requests match this filter.'; } function renderChats() { const filter = byId('chatFilter').value.trim().toLowerCase(); const rows = state.chats.filter(item => !filter || `${item.sender?.displayName} ${item.sender?.username} ${item.body} ${item.threadName} ${(item.members || []).map(x => x.displayName).join(' ')}`.toLowerCase().includes(filter)); byId('chatRows').innerHTML = rows.length ? rows.map(item => `
${esc(item.sender?.displayName || 'Unknown')}${esc(item.threadName || `Thread ${item.threadId}`)}${formatTime(item.createdAt)}
${esc(item.body)}
${esc((item.members || []).map(member => member.displayName).join(', '))}
`).join('') : '
No chats match this filter.
'; } function setupCanvas(canvas) { const ratio = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = Math.max(1, Math.round(rect.width * ratio)); canvas.height = Math.max(1, Math.round(rect.height * ratio)); const ctx = canvas.getContext('2d'); ctx.setTransform(ratio, 0, 0, ratio, 0, 0); return { ctx, width: rect.width, height: rect.height }; } function drawChart(canvas, series, lines) { const { ctx, width, height } = setupCanvas(canvas), pad = { left: 38, right: 18, top: 16, bottom: 25 }, plotW = width - pad.left - pad.right, plotH = height - pad.top - pad.bottom; ctx.clearRect(0, 0, width, height); ctx.strokeStyle = 'rgba(130,115,145,.18)'; ctx.lineWidth = 1; ctx.fillStyle = '#786f80'; ctx.font = '10px Inter, sans-serif'; for (let i = 0; i <= 4; i++) { const y = pad.top + plotH * i / 4; ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(width - pad.right, y); ctx.stroke(); } if (!series.length) return; const normalized = lines.map(line => ({ line, values: series.map(item => Number(line.value(item)) || 0) })); const max = Math.max(1, ...normalized.flatMap(item => item.values)) * 1.12; ctx.fillText(max >= 1048576 ? formatBytes(max) : max.toFixed(max < 10 ? 1 : 0), 3, pad.top + 4); ctx.fillText('0', 20, pad.top + plotH + 3); normalized.forEach(({ line, values }) => { ctx.beginPath(); ctx.strokeStyle = line.color; ctx.lineWidth = 2; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; values.forEach((value, index) => { const x = pad.left + (values.length === 1 ? plotW : plotW * index / (values.length - 1)), y = pad.top + plotH - (value / max * plotH); index ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }); ctx.stroke(); }); const first = new Date(series[0].at), last = new Date(series.at(-1).at); ctx.fillStyle = '#786f80'; ctx.fillText(formatTime(first), pad.left, height - 7); const label = formatTime(last); ctx.fillText(label, width - pad.right - ctx.measureText(label).width, height - 7); } function drawAllCharts() { if (!state.snapshot) return; const series = state.snapshot.series || []; drawChart(byId('requestChart'), series, [{ color: '#9c63f5', value: x => x.requests }, { color: '#ffab5c', value: x => x.latencyMs }]); drawChart(byId('networkChart'), series, [{ color: '#4da3ff', value: x => x.inboundBytes }, { color: '#42d6a4', value: x => x.outboundBytes }]); const resources = state.cpuHistory.map((point, index) => ({ at: point.at, cpu: point.value, memory: state.memoryHistory[index]?.value || 0 })); drawChart(byId('resourceChart'), resources, [{ color: '#9c63f5', value: x => x.cpu }, { color: '#f373d1', value: x => x.memory }]); } async function loadOlderChats() { const button = byId('loadOlderChats'), oldest = state.chats.length ? Math.min(...state.chats.map(item => Number(item.messageId))) : null; button.disabled = true; button.textContent = 'Loading…'; try { const url = `/recnet/api/developer/chats?take=100${oldest ? `&beforeMessageId=${oldest}` : ''}`; const response = await recnetFetch(url); if (response.status === 403) { location.href = '/recnet/'; return; } if (!response.ok) throw new Error('Could not load older chats.'); const data = await response.json(); state.chats = mergeBy(state.chats, data.messages || [], item => item.messageId, 10000); renderChats(); button.hidden = !data.hasMore; } catch (error) { button.textContent = error.message; return; } button.disabled = false; button.textContent = 'Load older messages'; } function connect() { state.stream?.close(); setStreamState('connecting', 'Connecting'); const stream = new EventSource('/recnet/api/developer/stream'); state.stream = stream; stream.addEventListener('snapshot', event => { setStreamState('', 'Live'); renderSnapshot(JSON.parse(event.data)); }); stream.onerror = () => { setStreamState('offline', 'Reconnecting'); }; } byId('requestFilter').addEventListener('input', renderRequests); byId('chatFilter').addEventListener('input', renderChats); byId('pauseRequests').addEventListener('click', event => { state.paused = !state.paused; event.currentTarget.classList.toggle('active', state.paused); event.currentTarget.textContent = state.paused ? 'Resume' : 'Pause'; }); byId('loadOlderChats').addEventListener('click', loadOlderChats); window.addEventListener('resize', () => requestAnimationFrame(drawAllCharts)); window.addEventListener('beforeunload', () => state.stream?.close()); connect();