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 = '
No packets yet.
' return } elements.packetList.innerHTML = state.events .slice() .reverse() .map((event) => { const active = event.id === state.selectedId ? ' active' : '' const forward = event.forward ? `forward: ${event.forward.status}` : 'local only' return ` ` }) .join('') } function matchesFilter(item) { if (!state.filter) { return true } const haystack = `${item.edge} ${item.tag} ${item.value}`.toLowerCase() return haystack.includes(state.filter) } function renderDetails() { const selected = getSelectedEvent() if (!selected) { elements.detailTitle.textContent = 'Waiting for ingest' elements.detailMeta.textContent = 'POST /api/ingest' elements.jsonMeta.textContent = 'No payload selected' elements.itemTable.innerHTML = '' elements.rawPayload.textContent = 'No payload yet.' return } const visibleItems = selected.items.filter(matchesFilter) elements.detailTitle.textContent = `${selected.count} values received` elements.detailMeta.textContent = `${formatTime(selected.receivedAt)} · ${formatBytes( selected.bytes, )}${selected.forward ? ` · forward: ${selected.forward.status}` : ''}` elements.jsonMeta.textContent = `${formatBytes(selected.bytes)} - ${selected.count} values` elements.detailMeta.textContent = `${formatTime(selected.receivedAt)} - ${formatBytes( selected.bytes, )}${selected.forward ? ` - forward: ${selected.forward.status}` : ''}` elements.rawPayload.textContent = getSelectedJson() if (elements.jsonModal.classList.contains('open')) { elements.jsonModalTitle.textContent = `${selected.count} values - ${formatTime( selected.receivedAt, )}` elements.jsonModalPayload.textContent = getSelectedJson() } elements.itemTable.innerHTML = visibleItems.length === 0 ? 'No matching values.' : visibleItems .map( (item) => ` ${item.tag} ${item.edge} ${item.value} ${formatTime(item.timestamp)} `, ) .join('') } function render() { elements.followButton.classList.toggle('active', state.followLatest) elements.followButton.textContent = state.followLatest ? 'Following latest' : 'Follow latest' renderPackets() renderDetails() } async function loadSnapshot() { const response = await fetch('/api/events') applySnapshot(await response.json()) } function connectEvents() { const source = new EventSource('/events') source.onopen = () => setConnectionStatus('live') source.onerror = () => setConnectionStatus('offline') source.onmessage = (message) => { const data = JSON.parse(message.data) if (data.snapshot) { applySnapshot(data.snapshot) return } if (data.type === 'ingest') { appendEvent(data.event) applySummary(data.summary) if (state.followLatest || !state.selectedId) { state.selectedId = data.event.id } render() } } } elements.packetList.addEventListener('click', (event) => { const packet = event.target.closest('.packet') if (!packet) { return } state.selectedId = packet.dataset.id state.followLatest = false render() }) elements.followButton.addEventListener('click', () => { state.followLatest = !state.followLatest if (state.followLatest && state.events.length > 0) { state.selectedId = state.events[state.events.length - 1].id } render() }) elements.filterInput.addEventListener('input', (event) => { state.filter = event.target.value.trim().toLowerCase() renderDetails() }) elements.copyButton.addEventListener('click', async () => { await copySelectedJson(elements.copyButton) }) elements.copyRawButton.addEventListener('click', async () => { await copySelectedJson(elements.copyRawButton) }) elements.expandJsonButton.addEventListener('click', () => { openJsonModal() }) elements.copyModalJsonButton.addEventListener('click', async () => { await copySelectedJson(elements.copyModalJsonButton) }) elements.closeJsonModalButton.addEventListener('click', () => { closeJsonModal() }) elements.jsonModalBackdrop.addEventListener('click', () => { closeJsonModal() }) document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeJsonModal() } }) elements.clearButton.addEventListener('click', async () => { await fetch('/api/events', { method: 'DELETE' }) state.events = [] state.selectedId = undefined state.followLatest = true render() await loadSnapshot() }) setConnectionStatus('connecting') await loadSnapshot() connectEvents()