const state = { events: [], selectedId: undefined, filter: '', connected: false, followLatest: true, maxEvents: 500, } const elements = { batchCount: document.querySelector('#batchCount'), itemCount: document.querySelector('#itemCount'), byteCount: document.querySelector('#byteCount'), lastBatch: document.querySelector('#lastBatch'), packetRate: document.querySelector('#packetRate'), packetList: document.querySelector('#packetList'), itemTable: document.querySelector('#itemTable'), rawPayload: document.querySelector('#rawPayload'), jsonMeta: document.querySelector('#jsonMeta'), expandJsonButton: document.querySelector('#expandJsonButton'), copyRawButton: document.querySelector('#copyRawButton'), jsonModal: document.querySelector('#jsonModal'), jsonModalBackdrop: document.querySelector('#jsonModalBackdrop'), jsonModalTitle: document.querySelector('#jsonModalTitle'), jsonModalPayload: document.querySelector('#jsonModalPayload'), copyModalJsonButton: document.querySelector('#copyModalJsonButton'), closeJsonModalButton: document.querySelector('#closeJsonModalButton'), detailTitle: document.querySelector('#detailTitle'), detailMeta: document.querySelector('#detailMeta'), connectionStatus: document.querySelector('#connectionStatus'), filterInput: document.querySelector('#filterInput'), followButton: document.querySelector('#followButton'), copyButton: document.querySelector('#copyButton'), clearButton: document.querySelector('#clearButton'), } const numberFormatter = new Intl.NumberFormat('ru-RU') const byteFormatter = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 1, }) function formatBytes(bytes) { if (bytes < 1024) { return `${bytes} B` } if (bytes < 1024 * 1024) { return `${byteFormatter.format(bytes / 1024)} KB` } return `${byteFormatter.format(bytes / 1024 / 1024)} MB` } function formatTime(timestamp) { return new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit', }).format(new Date(timestamp)) } function getSelectedEvent() { return state.events.find((event) => event.id === state.selectedId) } function getSelectedJson() { const selected = getSelectedEvent() return selected ? JSON.stringify(selected.payload, null, 2) : '' } async function copySelectedJson(button) { const json = getSelectedJson() if (!json) { return } const originalText = button.textContent await navigator.clipboard.writeText(json) button.textContent = 'Copied' setTimeout(() => { button.textContent = originalText }, 900) } function openJsonModal() { const selected = getSelectedEvent() if (!selected) { return } elements.jsonModalTitle.textContent = `${selected.count} values - ${formatTime( selected.receivedAt, )}` elements.jsonModalPayload.textContent = getSelectedJson() elements.jsonModal.classList.add('open') elements.jsonModal.setAttribute('aria-hidden', 'false') document.body.classList.add('modal-open') } function closeJsonModal() { elements.jsonModal.classList.remove('open') elements.jsonModal.setAttribute('aria-hidden', 'true') document.body.classList.remove('modal-open') } function setConnectionStatus(status) { state.connected = status === 'live' elements.connectionStatus.className = `status ${status}` elements.connectionStatus.querySelector('span:last-child').textContent = status } function applySnapshot(snapshot) { const previousSelectedId = state.selectedId state.events = snapshot.events ?? [] state.maxEvents = snapshot.maxEvents ?? state.maxEvents const latest = state.events[state.events.length - 1] const selectedStillExists = state.events.some( (event) => event.id === previousSelectedId, ) if (state.followLatest && latest) { state.selectedId = latest.id } else if (!selectedStillExists && latest) { state.selectedId = latest.id } else if (state.events.length === 0) { state.selectedId = undefined } elements.batchCount.textContent = numberFormatter.format(snapshot.receivedBatches ?? 0) elements.itemCount.textContent = numberFormatter.format(snapshot.receivedItems ?? 0) elements.byteCount.textContent = formatBytes(snapshot.receivedBytes ?? 0) elements.packetRate.textContent = `${numberFormatter.format( snapshot.recentBatchesPerMinute ?? 0, )}/min` elements.lastBatch.textContent = snapshot.latest ? `${snapshot.latest.count} items` : 'none' render() } function applySummary(summary) { state.maxEvents = summary.maxEvents ?? state.maxEvents elements.batchCount.textContent = numberFormatter.format(summary.receivedBatches ?? 0) elements.itemCount.textContent = numberFormatter.format(summary.receivedItems ?? 0) elements.byteCount.textContent = formatBytes(summary.receivedBytes ?? 0) elements.packetRate.textContent = `${numberFormatter.format( summary.recentBatchesPerMinute ?? 0, )}/min` elements.lastBatch.textContent = summary.latest ? `${summary.latest.count} items` : 'none' } function appendEvent(event) { state.events.push(event) while (state.events.length > state.maxEvents) { const removed = state.events.shift() if (removed?.id === state.selectedId) { state.selectedId = state.events[0]?.id } } } function renderPackets() { if (state.events.length === 0) { elements.packetList.innerHTML = '