Files
gh-christianlouis-dmarq/backend/app/templates/index.html
T

303 lines
16 KiB
HTML

{% extends "layouts/base.html" %}
{% from "components/ui/card.html" import card, card_header, card_title, card_description, card_content, card_footer %}
{% from "components/ui/button.html" import button, button_link %}
{% from "components/ui/alert.html" import alert, alert_title, alert_description %}
{% from "components/ui/table.html" import table, thead, tbody, tr, th, td %}
{% block title %}DMARQ - Dashboard{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="grid gap-4 md:gap-8 py-4" x-data="dashboardApp()">
<!-- Overview Stats -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Total Domains Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Total Domains{% endcall %}
<div class="p-2 bg-primary/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary"><circle cx="12" cy="12" r="10"></circle><line x1="2" x2="22" y1="12" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="total-domains" class="stat-value">0</div>
<p class="stat-description">Active domains being monitored</p>
</div>
{% endcall %}
{% endcall %}
<!-- Emails Analyzed Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Emails Analyzed{% endcall %}
<div class="p-2 bg-secondary/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-secondary"><rect width="20" height="16" x="2" y="4" rx="2"></rect><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="total-emails" class="stat-value">0</div>
<p class="stat-description">Total emails processed</p>
</div>
{% endcall %}
{% endcall %}
<!-- Pass Rate Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Pass Rate{% endcall %}
<div class="p-2 bg-green-500/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-500"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="overall-pass-rate" class="stat-value text-green-500">0%</div>
<p class="stat-description">Overall DMARC compliance</p>
</div>
{% endcall %}
{% endcall %}
<!-- Reports Card -->
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Reports Processed{% endcall %}
<div class="p-2 bg-accent/10 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-accent"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
</div>
</div>
{% endcall %}
{% call card_content() %}
<div class="stat-card">
<div id="reports-processed" class="stat-value">0</div>
<p class="stat-description">DMARC reports received</p>
</div>
{% endcall %}
{% endcall %}
</div>
<!-- Domain Compliance Table -->
<div x-show="hasDomainData" x-cloak>
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}Domain Compliance{% endcall %}
{% call button(variant="outline", size="sm") %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path></svg>
Refresh
{% endcall %}
</div>
{% call card_description() %}
Overview of domains and their DMARC compliance status
{% endcall %}
{% endcall %}
{% call card_content() %}
{% call table() %}
{% call thead() %}
{% call tr() %}
{% call th() %}Domain{% endcall %}
{% call th() %}Emails{% endcall %}
{% call th() %}Pass Rate{% endcall %}
{% call th() %}Failed{% endcall %}
{% call th() %}Reports{% endcall %}
{% call th("text-right") %}Actions{% endcall %}
{% endcall %}
{% endcall %}
{% call tbody() %}
<tbody id="domains-table-body">
<!-- Domain data will be populated here via JavaScript -->
</tbody>
{% endcall %}
{% endcall %}
{% endcall %}
{% endcall %}
</div>
<!-- No Data Message -->
<div x-show="!hasDomainData" x-cloak>
{% call card() %}
{% call card_content() %}
<div class="flex flex-col items-center justify-center py-12 text-center">
<div class="rounded-full p-4 bg-muted mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground"><circle cx="12" cy="12" r="10"></circle><path d="m8 12 4 0"></path><path d="m16 12 0 .01"></path><path d="M12 16v-4"></path></svg>
</div>
<h3 class="text-lg font-semibold mb-2">No DMARC reports have been uploaded yet</h3>
<p class="text-muted-foreground mb-4 max-w-md">Upload a report or configure IMAP integration to see statistics and gain insights into your domain's email authentication.</p>
<div class="flex gap-2">
{% call button_link(href="/upload") %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" x2="12" y1="3" y2="15"></line></svg>
Upload Report
{% endcall %}
{% call button_link(href="/settings", variant="outline") %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
Configure IMAP
{% endcall %}
</div>
</div>
{% endcall %}
{% endcall %}
</div>
<!-- IMAP Status -->
<div>
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between">
{% call card_title() %}IMAP Integration Status{% endcall %}
{% call button_link(href="/api/v1/admin/trigger-poll", variant="outline", size="sm") %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="m3 2 2 4h8L7 14h2l-4 8 1-6H4l2-8H3z"></path></svg>
Trigger Poll Now
{% endcall %}
</div>
{% endcall %}
{% call card_content() %}
<div class="flex flex-col gap-4">
<div class="flex items-center">
<span class="w-40 text-sm text-muted-foreground">Status:</span>
<span class="inline-flex items-center gap-1.5">
<span id="imap-status-icon" class="h-2.5 w-2.5 rounded-full bg-green-500"></span>
<span id="imap-status-text">Running</span>
</span>
</div>
<div class="flex items-center">
<span class="w-40 text-sm text-muted-foreground">Last Check:</span>
<span id="imap-last-check">Never</span>
</div>
<div class="flex items-center">
<span class="w-40 text-sm text-muted-foreground">Mailbox:</span>
<span id="imap-mailbox">dmarc-reports@hosterra.net</span>
</div>
</div>
{% endcall %}
{% endcall %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function dashboardApp() {
return {
hasDomainData: false,
init() {
// Fetch domain summary on page load
this.fetchDomainSummary();
// Check IMAP status
this.getImapStatus();
},
async getImapStatus() {
try {
const response = await fetch('/api/v1/poll-status');
if (!response.ok) {
console.error('Error checking IMAP status:', response.status);
return;
}
const data = await response.json();
const statusIcon = document.getElementById('imap-status-icon');
const statusText = document.getElementById('imap-status-text');
const lastCheck = document.getElementById('imap-last-check');
if (data.is_running) {
statusIcon.classList.remove('bg-red-500');
statusIcon.classList.add('bg-green-500');
statusText.textContent = 'Running';
} else {
statusIcon.classList.remove('bg-green-500');
statusIcon.classList.add('bg-red-500');
statusText.textContent = 'Stopped';
}
if (data.last_check) {
lastCheck.textContent = new Date(data.last_check).toLocaleString();
} else {
lastCheck.textContent = 'Never';
}
} catch (error) {
console.error('Error checking IMAP status:', error);
}
},
async fetchDomainSummary() {
try {
const response = await fetch('/api/v1/domains/summary');
const data = await response.json();
if (data && data.domains && data.domains.length > 0) {
this.hasDomainData = true;
this.updateDashboardStats(data);
this.populateDomainsTable(data.domains);
} else {
this.hasDomainData = false;
}
} catch (error) {
console.error('Error fetching domain summary:', error);
this.hasDomainData = false;
}
},
updateDashboardStats(data) {
if (!data) return;
const totalDomains = document.getElementById('total-domains');
const totalEmails = document.getElementById('total-emails');
const passRate = document.getElementById('overall-pass-rate');
const reportsProcessed = document.getElementById('reports-processed');
if (totalDomains) totalDomains.textContent = data.total_domains || 0;
if (totalEmails) totalEmails.textContent = data.total_emails || 0;
if (passRate) passRate.textContent = `${data.overall_pass_rate || 0}%`;
if (reportsProcessed) reportsProcessed.textContent = data.reports_processed || 0;
},
populateDomainsTable(domains) {
if (!domains || !domains.length) return;
const tableBody = document.getElementById('domains-table-body');
if (!tableBody) return;
tableBody.innerHTML = '';
domains.forEach(domain => {
const row = document.createElement('tr');
row.className = 'table-row';
row.innerHTML = `
<td class="table-cell">
<div class="font-medium">${domain.domain_name}</div>
</td>
<td class="table-cell">${domain.total_emails || 0}</td>
<td class="table-cell">
<span class="inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/20 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">
${domain.pass_rate || 0}%
</span>
</td>
<td class="table-cell">${domain.failed_count || 0}</td>
<td class="table-cell">${domain.report_count || 0}</td>
<td class="table-cell text-right">
<a href="/domains/${domain.id}" class="btn btn-outline btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg>
Details
</a>
</td>
`;
tableBody.appendChild(row);
});
}
}
}
</script>
{% endblock %}