Build Relay Lab reliability dashboard
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
||||
'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();
|
||||
Reference in New Issue
Block a user