'use strict'; const states = ['queued', 'running', 'delivered', 'dead']; const byId = (id) => document.getElementById(id); let lastRequest; let sending = false; function element(tag, value) { const node = document.createElement(tag); node.textContent = value; return node; } function renderList(id, items, toRow, emptyMessage) { byId(id).replaceChildren(...(items.length ? items.map(toRow) : [element('li', emptyMessage)])); } function render(state) { const latest = state.events[0]; byId('backend-event').textContent = latest ? `${latest.job_id.slice(0, 8)} · ${latest.message}` : 'No queue events yet.'; const { queued = 0, running = 0, delivered = 0, dead = 0 } = state.counts; byId('pipeline-queued').textContent = `${queued} waiting`; byId('pipeline-running').textContent = `${running} active / 2 workers`; byId('pipeline-outcome').textContent = `${delivered} delivered / ${dead} dead-lettered`; byId('pipeline').dataset.active = String(running > 0); byId('metrics').replaceChildren(...states.map((name) => { const cell = element('div', ''); cell.dataset.state = name; cell.append(element('dt', name), element('dd', state.counts[name] ?? 0)); return cell; })); renderList('jobs', state.jobs, (job) => { const row = element('li', ''); row.dataset.state = job.state; row.append( element('strong', `${job.scenario} · ${job.state}`), element('small', `${job.id.slice(0, 8)} / attempt ${job.attempts}`), ); return row; }, 'No jobs yet.'); renderList('events', state.events, (event) => { const row = element('li', ''); row.append( element('strong', event.message), element('small', `${new Date(event.at * 1000).toLocaleTimeString()} / ${event.job_id.slice(0, 8)}`), ); return row; }, 'No events yet.'); } async function poll() { const connection = byId('connection'); let online = false; try { const response = await fetch('/api/state'); if (!response.ok) throw new Error('Queue unavailable'); render(await response.json()); online = true; } catch { byId('pipeline').dataset.active = 'false'; byId('backend-event').textContent = 'Queue data unavailable.'; } finally { if (connection.dataset.online !== String(online)) { connection.textContent = online ? 'Queue connected' : 'Queue disconnected'; connection.dataset.online = String(online); } setTimeout(poll, 750); } } async function submit(request, button) { if (sending) return; sending = true; byId('backend-request').textContent = 'Sending request…'; if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) { button.animate( [{ transform: 'scale(.96)', boxShadow: '0 0 0 0 #bd93f9aa' }, { transform: 'scale(1)', boxShadow: '0 0 0 8px #bd93f900' }], { duration: 320, easing: 'ease-out' }, ); } try { const response = await fetch('/api/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), }); const result = await response.json(); byId('backend-request').textContent = `HTTP ${response.status} · ${JSON.stringify(result)}`; if (!response.ok) throw new Error(result.error || 'Request failed.'); lastRequest = request; byId('repeat').disabled = false; byId('status').textContent = `${result.created ? 'Queued job' : 'Reused job'} ${result.id.slice(0, 8)}.`; } catch (error) { if (byId('backend-request').textContent === 'Sending request…') { byId('backend-request').textContent = 'No API response available.'; } byId('status').textContent = error.message; } finally { sending = false; } } for (const button of document.querySelectorAll('[data-scenario]')) { button.addEventListener('click', () => submit({ key: crypto.randomUUID(), scenario: button.dataset.scenario }, button)); } byId('repeat').addEventListener('click', (event) => submit(lastRequest, event.currentTarget)); poll();