Merge pull request #106 from christianlouis/codex/dashboard-top-sources
feat: add dashboard top source report
This commit is contained in:
@@ -116,6 +116,38 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Sending Sources -->
|
||||||
|
<div x-show="hasDomainData" x-cloak>
|
||||||
|
{% 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() %}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
{% 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() %}
|
||||||
|
<tbody id="top-sources-table-body">
|
||||||
|
<!-- Source data will be populated here via JavaScript -->
|
||||||
|
</tbody>
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Domain Compliance Table -->
|
<!-- Domain Compliance Table -->
|
||||||
<div x-show="hasDomainData" x-cloak>
|
<div x-show="hasDomainData" x-cloak>
|
||||||
{% call card() %}
|
{% call card() %}
|
||||||
@@ -273,30 +305,33 @@ function dashboardApp() {
|
|||||||
this.hasDomainData = true;
|
this.hasDomainData = true;
|
||||||
this.updateDashboardStats(data);
|
this.updateDashboardStats(data);
|
||||||
this.populateDomainsTable(data.domains);
|
this.populateDomainsTable(data.domains);
|
||||||
this.$nextTick(() => this.fetchDashboardTrends());
|
this.$nextTick(() => this.fetchDashboardStats());
|
||||||
} else {
|
} else {
|
||||||
this.hasDomainData = false;
|
this.hasDomainData = false;
|
||||||
this.clearDashboardCharts();
|
this.clearDashboardCharts();
|
||||||
|
this.populateTopSources([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching domain summary:', error);
|
console.error('Error fetching domain summary:', error);
|
||||||
this.hasDomainData = false;
|
this.hasDomainData = false;
|
||||||
this.clearDashboardCharts();
|
this.clearDashboardCharts();
|
||||||
|
this.populateTopSources([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchDashboardTrends() {
|
async fetchDashboardStats() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/stats/dashboard?period_days=30');
|
const response = await fetch('/api/v1/stats/dashboard?period_days=30');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Error fetching dashboard trends:', response.status);
|
console.error('Error fetching dashboard stats:', response.status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.renderDashboardCharts(data.compliance_trend || []);
|
this.renderDashboardCharts(data.compliance_trend || []);
|
||||||
|
this.populateTopSources(data.top_sources || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching dashboard trends:', error);
|
console.error('Error fetching dashboard stats:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -472,6 +507,91 @@ function dashboardApp() {
|
|||||||
return new Intl.NumberFormat(undefined, { notation: 'compact' }).format(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) {
|
populateDomainsTable(domains) {
|
||||||
if (!domains || !domains.length) return;
|
if (!domains || !domains.length) return;
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ Priority tasks:
|
|||||||
|
|
||||||
Delivered:
|
Delivered:
|
||||||
- Dashboard time-series charts show daily mail volume, compliance rate, and failure rate.
|
- 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:
|
Quality bar:
|
||||||
- A domain owner can understand who sends mail as their domain, which sources fail, and what to fix next.
|
- A domain owner can understand who sends mail as their domain, which sources fail, and what to fix next.
|
||||||
|
|||||||
+1
-1
@@ -79,9 +79,9 @@ Goal: convert raw DMARC data into useful operational reporting.
|
|||||||
|
|
||||||
Delivered:
|
Delivered:
|
||||||
- Dashboard trend charts for volume, compliance rate, and failure rate.
|
- Dashboard trend charts for volume, compliance rate, and failure rate.
|
||||||
|
- Top sender/source reports with pass/fail breakdowns.
|
||||||
|
|
||||||
Planned:
|
Planned:
|
||||||
- Top sender/source reports with pass/fail breakdowns.
|
|
||||||
- Per-domain report timeline and daily rollups.
|
- Per-domain report timeline and daily rollups.
|
||||||
- Exportable reports for a selected domain and date range.
|
- 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.
|
- Clear recommendations for common cases: unknown source, SPF-only pass, DKIM-only pass, full fail, and policy not enforced.
|
||||||
|
|||||||
+2
-2
@@ -131,14 +131,14 @@ This file tracks the specific implementation tasks for each milestone of the DMA
|
|||||||
- [x] Integrate Chart.js library
|
- [x] Integrate Chart.js library
|
||||||
- [x] Create time-series charts for DMARC compliance
|
- [x] Create time-series charts for DMARC compliance
|
||||||
- [x] Add volume charts for email traffic
|
- [x] Add volume charts for email traffic
|
||||||
- [ ] Implement sender breakdown visualizations
|
- [x] Implement sender breakdown visualizations
|
||||||
- [ ] Create policy distribution charts
|
- [ ] Create policy distribution charts
|
||||||
|
|
||||||
### Dashboard Widgets
|
### Dashboard Widgets
|
||||||
- [ ] Create compliance rate summary widget
|
- [ ] Create compliance rate summary widget
|
||||||
- [ ] Add enforcement rate widget
|
- [ ] Add enforcement rate widget
|
||||||
- [x] Implement email volume trends 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)
|
- [ ] Add alert status summary (for later integration)
|
||||||
|
|
||||||
### Historical Data
|
### Historical Data
|
||||||
|
|||||||
Reference in New Issue
Block a user