Admin SPA: surface webhook delivery history (DLQ visible)

The /v1/admin/webhook-deliveries endpoints from v0.1.0:43 were
operator-actionable via curl but invisible in the dashboard. Adds a
"Delivery history" section to the Webhooks page showing recent
deliveries with a status filter (defaults to "Failed (DLQ)" so the
problem case is what an operator sees first).

Each row shows created-at, event type, status badge (delivered /
failed / pending), attempt count, last status code, and last_error
inline beneath the status when present (so operators don't have to
chase a separate "details" view to know why a delivery failed).
Non-delivered rows get a Retry button that re-queues via the
existing POST /v1/admin/webhook-deliveries/:id/retry; the worker
picks up the retried row on its next 5s tick.

No backend changes. The endpoints landed in :43; this commit is
just the front-end surface.
This commit is contained in:
Grant
2026-05-08 10:41:44 -05:00
parent 5ec9a6e8c0
commit 4adf5a8593
+89
View File
@@ -2089,6 +2089,95 @@ The request will be refused if there are licenses or invoices tied to it — use
} catch (e) { } catch (e) {
target.appendChild(plainCard([err(e.message)])) target.appendChild(plainCard([err(e.message)]))
} }
// ----- Delivery history -----
//
// Outbound deliveries get retried with exponential backoff up to
// 10 attempts; after that they're "dead-lettered" — sat in the
// DB unreachable. The new admin endpoint (v0.1.0:43) exposes them
// so operators can investigate and manually re-queue.
const status = el('select', { class: 'input', style: 'min-width:10rem' }, [
el('option', { value: 'all' }, 'All'),
el('option', { value: 'pending' }, 'Pending (in retry queue)'),
el('option', { value: 'delivered' }, 'Delivered'),
el('option', { value: 'failed', selected: 'selected' }, 'Failed (DLQ)'),
])
const reload = el('button', { class: 'btn sm secondary', onclick: () => loadDeliveries() }, 'Reload')
const deliveriesContainer = el('div')
async function loadDeliveries () {
deliveriesContainer.innerHTML = ''
deliveriesContainer.appendChild(el('p', { class: 'muted' }, 'Loading…'))
try {
const params = new URLSearchParams({ status: status.value, limit: '100' })
const j = await api('/v1/admin/webhook-deliveries?' + params.toString())
const ds = j.deliveries || []
deliveriesContainer.innerHTML = ''
if (ds.length === 0) {
const empty = status.value === 'failed'
? 'No failed deliveries — all webhooks are landing or in flight.'
: 'No deliveries match this filter.'
deliveriesContainer.appendChild(el('p', { class: 'muted', style: 'margin:8px 0' }, empty))
return
}
const rows = ds.map((d) => {
// status: delivered (delivered_at set) | failed (no next + attempts > 0) | pending (next set)
let pillCls = 'badge b-warning'
let pillDot = 'warn'
let pillText = 'pending'
if (d.delivered_at) {
pillCls = 'badge b-success'
pillDot = 'ok'
pillText = 'delivered'
} else if (!d.next_attempt_at && d.attempt_count > 0) {
pillCls = 'badge b-danger'
pillDot = 'err'
pillText = 'failed (DLQ)'
}
const pill = el('span', { class: pillCls }, [el('span', { class: 'dot ' + pillDot }), pillText])
const lastErr = d.last_error
? el('div', { class: 'muted', style: 'font-size:0.85em; margin-top:4px; word-break:break-all' }, d.last_error)
: null
return el('tr', null, [
el('td', { class: 'muted' }, fmtDate(d.created_at)),
el('td', null, d.event_type),
el('td', null, [pill, lastErr].filter(Boolean)),
el('td', { class: 'muted' }, String(d.attempt_count)),
el('td', { class: 'muted' }, d.last_status_code ? String(d.last_status_code) : '—'),
el('td', null, d.delivered_at
? null
: el('button', { class: 'btn sm secondary', onclick: async () => {
try {
await api('/v1/admin/webhook-deliveries/' + d.id + '/retry', { method: 'POST' })
loadDeliveries()
} catch (er) { alert(er.message) }
}}, 'Retry')),
])
})
deliveriesContainer.appendChild(el('table', { class: 'data' }, [
el('thead', null, el('tr', null,
['Created', 'Event', 'Status', 'Attempts', 'Last code', '']
.map((h) => el('th', null, h))
)),
el('tbody', null, rows),
]))
} catch (e) {
deliveriesContainer.innerHTML = ''
deliveriesContainer.appendChild(err(e.message))
}
}
status.addEventListener('change', loadDeliveries)
target.appendChild(plainCard([
el('h3', { style: 'margin:0 0 8px' }, 'Delivery history'),
el('p', { class: 'muted', style: 'margin:0 0 12px' },
'Defaults to "Failed" so the DLQ is visible at a glance. Failed deliveries are dead-lettered after 10 retry attempts (~7h backoff window). "Retry" re-queues the delivery for the worker on its next 5s tick.'),
el('div', { style: 'display:flex; gap:8px; align-items:center; margin-bottom:12px' }, [status, reload]),
deliveriesContainer,
]))
loadDeliveries()
} }
// -------- Audit log -------- // -------- Audit log --------