From 692ca913e0857a7044f33ce138d9c1f090b4d26b Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Fri, 22 May 2026 21:01:14 +0200 Subject: [PATCH] feat: add dashboard top source report --- backend/app/templates/index.html | 128 ++++++++++++++++++++++++++++++- docs/development/roadmap.md | 1 + docs/milestones.md | 2 +- docs/todo.md | 4 +- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/backend/app/templates/index.html b/backend/app/templates/index.html index 3c12e34..a7304e1 100644 --- a/backend/app/templates/index.html +++ b/backend/app/templates/index.html @@ -115,6 +115,38 @@ {% endcall %} {% endcall %} + + +
+ {% call card() %} + {% call card_header() %} + {% call card_title() %}Top Sending Sources{% endcall %} + {% call card_description() %} + Sender IPs with authentication pass and fail breakdowns + {% endcall %} + {% endcall %} + {% call card_content() %} +
+ {% call table() %} + {% call thead() %} + {% call tr() %} + {% call th() %}Source{% endcall %} + {% call th() %}Messages{% endcall %} + {% call th() %}DMARC{% endcall %} + {% call th() %}SPF{% endcall %} + {% call th() %}DKIM{% endcall %} + {% endcall %} + {% endcall %} + {% call tbody() %} + + + + {% endcall %} + {% endcall %} +
+ {% endcall %} + {% endcall %} +
@@ -273,30 +305,33 @@ function dashboardApp() { this.hasDomainData = true; this.updateDashboardStats(data); this.populateDomainsTable(data.domains); - this.$nextTick(() => this.fetchDashboardTrends()); + this.$nextTick(() => this.fetchDashboardStats()); } else { this.hasDomainData = false; this.clearDashboardCharts(); + this.populateTopSources([]); } } catch (error) { console.error('Error fetching domain summary:', error); this.hasDomainData = false; this.clearDashboardCharts(); + this.populateTopSources([]); } }, - async fetchDashboardTrends() { + async fetchDashboardStats() { try { const response = await fetch('/api/v1/stats/dashboard?period_days=30'); if (!response.ok) { - console.error('Error fetching dashboard trends:', response.status); + console.error('Error fetching dashboard stats:', response.status); return; } const data = await response.json(); this.renderDashboardCharts(data.compliance_trend || []); + this.populateTopSources(data.top_sources || []); } catch (error) { - console.error('Error fetching dashboard trends:', error); + console.error('Error fetching dashboard stats:', error); } }, @@ -471,6 +506,91 @@ function dashboardApp() { formatLargeNumber(value) { return new Intl.NumberFormat(undefined, { notation: 'compact' }).format(value); }, + + populateTopSources(sources) { + const tableBody = document.getElementById('top-sources-table-body'); + if (!tableBody) return; + + tableBody.textContent = ''; + + if (!sources || !sources.length) { + const emptyRow = document.createElement('tr'); + emptyRow.className = 'table-row'; + const emptyCell = document.createElement('td'); + emptyCell.className = 'table-cell text-muted-foreground'; + emptyCell.colSpan = 5; + emptyCell.textContent = 'No sending source data available'; + emptyRow.appendChild(emptyCell); + tableBody.appendChild(emptyRow); + return; + } + + sources.forEach(source => { + const row = document.createElement('tr'); + row.className = 'table-row'; + + row.appendChild(this.createTextCell(source.ip || 'Unknown', 'font-mono text-sm')); + row.appendChild(this.createTextCell(this.formatLargeNumber(source.count || 0))); + row.appendChild(this.createAuthCell( + source.dmarc, + source.dmarc_pass_count || 0, + source.dmarc_fail_count || 0 + )); + row.appendChild(this.createAuthCell( + source.spf, + source.spf_pass_count || 0, + source.spf_fail_count || 0 + )); + row.appendChild(this.createAuthCell( + source.dkim, + source.dkim_pass_count || 0, + source.dkim_fail_count || 0 + )); + + tableBody.appendChild(row); + }); + }, + + createTextCell(value, innerClass = '') { + const cell = document.createElement('td'); + cell.className = 'table-cell'; + const content = document.createElement('span'); + if (innerClass) content.className = innerClass; + content.textContent = value; + cell.appendChild(content); + return cell; + }, + + createAuthCell(status, passCount, failCount) { + const cell = document.createElement('td'); + cell.className = 'table-cell'; + + const wrapper = document.createElement('div'); + wrapper.className = 'flex flex-col gap-1'; + wrapper.appendChild(this.createStatusBadge(status)); + + const counts = document.createElement('span'); + counts.className = 'text-xs text-muted-foreground whitespace-nowrap'; + counts.textContent = `${this.formatLargeNumber(passCount)} pass / ${this.formatLargeNumber(failCount)} fail`; + wrapper.appendChild(counts); + + cell.appendChild(wrapper); + return cell; + }, + + createStatusBadge(status) { + const normalized = status || 'none'; + const badge = document.createElement('span'); + const styles = { + pass: 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300', + fail: 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300', + mixed: 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300', + none: 'bg-muted text-muted-foreground' + }; + badge.className = `inline-flex w-fit items-center rounded-md px-2 py-1 text-xs font-medium ${styles[normalized] || styles.none}`; + badge.textContent = normalized.charAt(0).toUpperCase() + normalized.slice(1); + return badge; + }, populateDomainsTable(domains) { if (!domains || !domains.length) return; diff --git a/docs/development/roadmap.md b/docs/development/roadmap.md index 8aacb8d..1762381 100644 --- a/docs/development/roadmap.md +++ b/docs/development/roadmap.md @@ -48,6 +48,7 @@ Priority tasks: Delivered: - Dashboard time-series charts show daily mail volume, compliance rate, and failure rate. +- Top sending sources show DMARC, SPF, and DKIM pass/fail breakdowns on the dashboard. Quality bar: - A domain owner can understand who sends mail as their domain, which sources fail, and what to fix next. diff --git a/docs/milestones.md b/docs/milestones.md index 94b671b..86a7265 100644 --- a/docs/milestones.md +++ b/docs/milestones.md @@ -79,9 +79,9 @@ Goal: convert raw DMARC data into useful operational reporting. Delivered: - Dashboard trend charts for volume, compliance rate, and failure rate. +- Top sender/source reports with pass/fail breakdowns. Planned: -- Top sender/source reports with pass/fail breakdowns. - Per-domain report timeline and daily rollups. - Exportable reports for a selected domain and date range. - Clear recommendations for common cases: unknown source, SPF-only pass, DKIM-only pass, full fail, and policy not enforced. diff --git a/docs/todo.md b/docs/todo.md index cc691c1..e715a5d 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -131,14 +131,14 @@ This file tracks the specific implementation tasks for each milestone of the DMA - [x] Integrate Chart.js library - [x] Create time-series charts for DMARC compliance - [x] Add volume charts for email traffic -- [ ] Implement sender breakdown visualizations +- [x] Implement sender breakdown visualizations - [ ] Create policy distribution charts ### Dashboard Widgets - [ ] Create compliance rate summary widget - [ ] Add enforcement rate widget - [x] Implement email volume trends widget -- [ ] Create top sender sources widget +- [x] Create top sender sources widget - [ ] Add alert status summary (for later integration) ### Historical Data