diff --git a/licensing-service/web/index.html b/licensing-service/web/index.html
index 1dfbd35..7ecc9ac 100644
--- a/licensing-service/web/index.html
+++ b/licensing-service/web/index.html
@@ -2089,6 +2089,95 @@ The request will be refused if there are licenses or invoices tied to it — use
} catch (e) {
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 --------