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