{% 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: [], versions: { total_families: 0, total_actions: 0, by_status: { active: 0, deprecated: 0, retired: 0 }, subscriptions_on_deprecated: 0, at_risk: [] }, 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(); this.loadVersions(); }, 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(() => {}); }, loadVersions(){ fetch('/v1/metrics/versions', { credentials: 'include' }) .then(r => r.json()) .then(data => { this.versions = data; }) .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
Action families
Active
Deprecated
Retired
Subs on deprecated/retired
Migration debt — deprecated/retired actions with active subscribers
Action Family Status Successor Subscribers
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 %}