{% extends 'clean_base.axml' %} {% set modal_name = 'Dashboard' %} {% block head %} {% endblock %} {% macro dashctrl() %} { window: 'all', view: 'charts', // 'charts' | 'tables' tablesTab: 'failures', // 'failures' | 'subs' (only matters in tables view) metrics: { published: 0, delivered: 0, retrying: 0, pending: 0, failed: 0, total: 0, success_rate: null }, failures: [], subs: [], loading: false, busy: null, gistDetail: null, gistDetailIDE: null, subDetail: null, subGists: [], subLoading: false, showBackfill: false, backfillSub: null, backfillSubName: '', backfillSince: '', backfillCount: null, backfillRunning: false, init(){ this.refresh(); }, refresh(){ this.loadMetrics(); this.loadFailures(); this.loadSubs(); }, loadMetrics(){ fetch(`/v1/metrics/deliveries?window=${this.window}`, { credentials: 'include' }) .then(r => r.json()) .then(data => { this.metrics = data; this.renderCharts(); }) .catch(() => ShowFeedback('error', 'Failed to load metrics')); }, loadFailures(){ this.loading = true; fetch('/v1/gists?status=failed&pagination=200', { credentials: 'include' }) .then(r => r.json()) .then(data => { this.failures = data.data || []; this.loading = false; this.renderCharts(); }) .catch(() => { this.loading = false; }); }, loadSubs(){ fetch('/v1/metrics/subscriptions', { credentials: 'include' }) .then(r => r.json()) .then(data => { this.subs = data.data || []; this.renderCharts(); }) .catch(() => {}); }, setWindow(w){ this.window = w; this.loadMetrics(); }, setView(v){ this.view = v; if(v === 'charts') this.renderCharts(); }, _groupCount(items, keyfn){ const counts = {}; items.forEach(it => { const k = keyfn(it) || 'unknown'; counts[k] = (counts[k] || 0) + 1; }); return Object.entries(counts).sort((a, b) => b[1] - a[1]); }, _bar(sel, pairs, klass){ const el = document.querySelector(sel); if(!el) return; el.innerHTML = ''; if(!pairs.length){ el.textContent = 'No data'; el.classList.add('chart-empty'); return; } el.classList.remove('chart-empty'); new Chartist.Bar(sel, { labels: pairs.map(p => p[0].length > 22 ? p[0].slice(0, 21) + '…' : p[0]), series: [pairs.map(p => p[1])], }, { horizontalBars: true, reverseData: true, axisX: { onlyInteger: true }, axisY: { offset: 150 }, chartPadding: { right: 24 } }); el.classList.add(klass); }, renderCharts(){ if(this.view !== 'charts') return; this.$nextTick(() => { // Delivery status donut const m = this.metrics; const donut = document.querySelector('#chart-status'); if(donut){ donut.innerHTML = ''; const total = (m.delivered||0)+(m.retrying||0)+(m.pending||0)+(m.failed||0); if(total === 0){ donut.textContent = 'No deliveries'; donut.classList.add('chart-empty'); } else { donut.classList.remove('chart-empty'); new Chartist.Pie('#chart-status', { series: [ { value: m.delivered||0, className: 'ct-delivered' }, { value: m.retrying||0, className: 'ct-retrying' }, { value: m.pending||0, className: 'ct-pending' }, { value: m.failed||0, className: 'ct-failed' }, ]}, { donut: true, donutWidth: 46, showLabel: false, chartPadding: 6 }); } } // Top failing subscriptions const topSubs = [...this.subs].filter(s => s.failed > 0).sort((a, b) => b.failed - a.failed).slice(0, 8) .map(s => [`${s.subscriber} · ${s.action}`, s.failed]); this._bar('#chart-subs', topSubs, 'dash-bars'); // Failed deliveries by action / by workspace (from the failed sample) this._bar('#chart-actions', this._groupCount(this.failures, g => g.action).slice(0, 8), 'dash-bars'); this._bar('#chart-workspaces', this._groupCount(this.failures, g => g.workspace).slice(0, 8), 'dash-bars-warn'); }); }, fmtRate(){ return this.metrics.success_rate === null ? '—' : `${this.metrics.success_rate}%`; }, replayOne(id){ this.busy = id; fetch(`/v1/regists/${id}`, { method: 'POST', credentials: 'include' }) .then(async r => { if(!r.ok) throw await r.json().catch(() => ({})); return r.json(); }) .then(data => { if(data.delivered) ShowFeedback('success', `Gist ${id} replayed — delivered (${data.status_code})`); else ShowFeedback('warning', `Gist ${id} replayed but still failing${data.status_code ? ' ('+data.status_code+')' : ''}`); this.refresh(); }) .catch(err => ShowFeedback('error', err?.error || 'Replay failed')) .finally(() => { this.busy = null; }); }, requeueOne(id){ this.busy = id; fetch(`/v1/requeues?gist=${id}`, { method: 'POST', credentials: 'include' }) .then(async r => { if(!r.ok) throw await r.json().catch(() => ({})); return r.json(); }) .then(data => { ShowFeedback('success', `Gist ${id} requeued — daemon will retry it`); this.refresh(); }) .catch(err => ShowFeedback('error', err?.error || 'Requeue failed')) .finally(() => { this.busy = null; }); }, statusClass(s){ return { delivered: 'success', failed: 'danger', retrying: 'warning', pending: '' }[s] || ''; }, fmtDate(v){ return v ? new Date(String(v).replace(' ', 'T')).toLocaleString() : '—'; }, openGist(g){ this.gistDetail = g; this.$nextTick(() => { const el = document.getElementById('dash-gist-ide'); if(!el) return; this.gistDetailIDE = ace.edit(el); this.gistDetailIDE.session.setMode('ace/mode/json'); const dark = document.documentElement.getAttribute('data-bs-theme') === 'dark'; this.gistDetailIDE.setTheme(dark ? 'ace/theme/one_dark' : 'ace/theme/chrome'); this.gistDetailIDE.setOptions({ readOnly: true, fontSize: 13, showPrintMargin: false, wrap: true, tabSize: 2 }); this.gistDetailIDE.setValue(prettyJson(g.payload || {})); this.gistDetailIDE.clearSelection(); }); }, closeGist(){ this.gistDetail = null; if(this.gistDetailIDE){ this.gistDetailIDE.destroy(); this.gistDetailIDE = null; } }, openSub(s){ this.subDetail = s; this.subGists = []; this.subLoading = true; fetch(`/v1/gists?subscription=${encodeURIComponent(s.subscription)}&pagination=100`, { credentials: 'include' }) .then(r => r.json()) .then(data => { this.subGists = data.data || []; this.subLoading = false; }) .catch(() => { this.subLoading = false; ShowFeedback('error', 'Failed to load subscription deliveries'); }); }, openBackfill(sub){ this.backfillSub = sub.subscription; this.backfillSubName = `${sub.subscriber} · ${sub.action}`; this.backfillSince = ''; this.backfillCount = null; this.showBackfill = true; this.dryBackfill(); }, dryBackfill(){ fetch('/v1/backfills', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ subscription: this.backfillSub, since: this.backfillSince, dry_run: true }) }).then(r => r.json()).then(data => { this.backfillCount = data.count; }) .catch(() => ShowFeedback('error', 'Could not compute backfill count')); }, runBackfill(){ this.backfillRunning = true; fetch('/v1/backfills', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ subscription: this.backfillSub, since: this.backfillSince }) }).then(r => r.json()).then(data => { this.backfillRunning = false; this.showBackfill = false; ShowFeedback('success', `Backfilling ${data.backfilled} historical events to this subscription`); this.refresh(); }).catch(() => { this.backfillRunning = false; ShowFeedback('error', 'Backfill failed'); }); } } {% endmacro %} {% block easel %}
Published
Delivered
Retrying
Failed
Success rate
Delivery status
Delivered Retrying Pending Failed
Top failing subscriptions
Failures by action (current failed set)
Failures by workspace (current failed set)
Action Workspace Publisher → Subscriber Endpoint Attempts Code Last error Last attempt Actions
Subscriber Action Endpoint Delivered Failed Most recent error Backfill
{% endblock %}