866 lines
51 KiB
HTML
866 lines
51 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DMARQ - DMARC Monitoring</title>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Font Awesome for icons -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
|
|
|
|
<!-- Inter font -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="/static/css/styles.css">
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
container: {
|
|
center: true,
|
|
padding: "2rem",
|
|
screens: {
|
|
"2xl": "1400px",
|
|
},
|
|
},
|
|
extend: {
|
|
colors: {
|
|
border: "hsl(var(--border))",
|
|
input: "hsl(var(--input))",
|
|
ring: "hsl(var(--ring))",
|
|
background: "hsl(var(--background))",
|
|
foreground: "hsl(var(--foreground))",
|
|
primary: {
|
|
DEFAULT: "hsl(var(--primary))",
|
|
foreground: "hsl(var(--primary-foreground))",
|
|
},
|
|
secondary: {
|
|
DEFAULT: "hsl(var(--secondary))",
|
|
foreground: "hsl(var(--secondary-foreground))",
|
|
},
|
|
destructive: {
|
|
DEFAULT: "hsl(var(--destructive))",
|
|
foreground: "hsl(var(--destructive-foreground))",
|
|
},
|
|
muted: {
|
|
DEFAULT: "hsl(var(--muted))",
|
|
foreground: "hsl(var(--muted-foreground))",
|
|
},
|
|
accent: {
|
|
DEFAULT: "hsl(var(--accent))",
|
|
foreground: "hsl(var(--accent-foreground))",
|
|
},
|
|
popover: {
|
|
DEFAULT: "hsl(var(--popover))",
|
|
foreground: "hsl(var(--popover-foreground))",
|
|
},
|
|
card: {
|
|
DEFAULT: "hsl(var(--card))",
|
|
foreground: "hsl(var(--card-foreground))",
|
|
},
|
|
},
|
|
borderRadius: {
|
|
lg: "var(--radius)",
|
|
md: "calc(var(--radius) - 2px)",
|
|
sm: "calc(var(--radius) - 4px)",
|
|
},
|
|
fontFamily: {
|
|
sans: ["Inter", "sans-serif"],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 240 10% 3.9%;
|
|
--card: 0 0% 100%;
|
|
--card-foreground: 240 10% 3.9%;
|
|
--popover: 0 0% 100%;
|
|
--popover-foreground: 240 10% 3.9%;
|
|
--primary: 232 47% 49%;
|
|
--primary-foreground: 0 0% 98%;
|
|
--secondary: 187 100% 42%;
|
|
--secondary-foreground: 240 5.9% 10%;
|
|
--muted: 240 4.8% 95.9%;
|
|
--muted-foreground: 240 3.8% 46.1%;
|
|
--accent: 240 4.8% 95.9%;
|
|
--accent-foreground: 240 5.9% 10%;
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
--border: 240 5.9% 90%;
|
|
--input: 240 5.9% 90%;
|
|
--ring: 232 47% 49%;
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
.dark {
|
|
--background: 240 10% 3.9%;
|
|
--foreground: 0 0% 98%;
|
|
--card: 240 10% 3.9%;
|
|
--card-foreground: 0 0% 98%;
|
|
--popover: 240 10% 3.9%;
|
|
--popover-foreground: 0 0% 98%;
|
|
--primary: 232 47% 49%;
|
|
--primary-foreground: 0 0% 98%;
|
|
--secondary: 187 100% 42%;
|
|
--secondary-foreground: 0 0% 98%;
|
|
--muted: 240 3.7% 15.9%;
|
|
--muted-foreground: 240 5% 64.9%;
|
|
--accent: 240 3.7% 15.9%;
|
|
--accent-foreground: 0 0% 98%;
|
|
--destructive: 0 62.8% 30.6%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
--border: 240 3.7% 15.9%;
|
|
--input: 240 3.7% 15.9%;
|
|
--ring: 232 47% 49%;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen bg-background font-sans antialiased">
|
|
<!-- Alpine.js for reactivity -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<div x-data="dashboardApp()">
|
|
<!-- Sidebar -->
|
|
<div class="fixed inset-y-0 left-0 z-20 w-64 bg-card border-r border-border shadow-sm transform transition-transform duration-300 lg:translate-x-0"
|
|
:class="{'translate-x-0': sidebarOpen, '-translate-x-full': !sidebarOpen}">
|
|
|
|
<!-- Logo -->
|
|
<div class="flex items-center justify-between p-4 border-b border-border">
|
|
<div class="flex items-center space-x-2">
|
|
<svg class="w-8 h-8 text-primary" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14h-2V9h2v8zm4 0h-2v-4h2v4zm0-6h-2V9h2v2zm-8 2H6v4h2v-4zm0-4H6v2h2V9z"/>
|
|
</svg>
|
|
<div class="text-xl font-bold text-foreground">DMARQ</div>
|
|
</div>
|
|
<button @click="sidebarOpen = false" type="button" class="p-1 text-muted-foreground rounded-md lg:hidden hover:bg-accent hover:text-accent-foreground">
|
|
<span class="sr-only">Close sidebar</span>
|
|
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="flex-1 p-2 overflow-y-auto">
|
|
<ul class="space-y-1">
|
|
<li>
|
|
<a href="#" @click.prevent="activeTab = 'dashboard'"
|
|
class="flex items-center w-full px-3 py-2 transition-colors rounded-md text-sm font-medium"
|
|
:class="activeTab === 'dashboard' ? 'bg-primary text-primary-foreground' : 'text-foreground hover:bg-accent hover:text-accent-foreground'">
|
|
<i class="fas fa-tachometer-alt w-4 h-4 mr-2"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click.prevent="activeTab = 'upload'"
|
|
class="flex items-center w-full px-3 py-2 transition-colors rounded-md text-sm font-medium"
|
|
:class="activeTab === 'upload' ? 'bg-primary text-primary-foreground' : 'text-foreground hover:bg-accent hover:text-accent-foreground'">
|
|
<i class="fas fa-upload w-4 h-4 mr-2"></i>
|
|
<span>Upload Reports</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click.prevent="activeTab = 'domains'"
|
|
class="flex items-center w-full px-3 py-2 transition-colors rounded-md text-sm font-medium"
|
|
:class="activeTab === 'domains' ? 'bg-primary text-primary-foreground' : 'text-foreground hover:bg-accent hover:text-accent-foreground'">
|
|
<i class="fas fa-globe w-4 h-4 mr-2"></i>
|
|
<span>Domains</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click.prevent="activeTab = 'reports'"
|
|
class="flex items-center w-full px-3 py-2 transition-colors rounded-md text-sm font-medium"
|
|
:class="activeTab === 'reports' ? 'bg-primary text-primary-foreground' : 'text-foreground hover:bg-accent hover:text-accent-foreground'">
|
|
<i class="fas fa-chart-bar w-4 h-4 mr-2"></i>
|
|
<span>Reports</span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" @click.prevent="activeTab = 'settings'"
|
|
class="flex items-center w-full px-3 py-2 transition-colors rounded-md text-sm font-medium"
|
|
:class="activeTab === 'settings' ? 'bg-primary text-primary-foreground' : 'text-foreground hover:bg-accent hover:text-accent-foreground'">
|
|
<i class="fas fa-cog w-4 h-4 mr-2"></i>
|
|
<span>Settings</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<div class="p-4 text-xs text-center text-muted-foreground">
|
|
<div>DMARQ v0.1.0</div>
|
|
<div>DMARC Monitoring Platform</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="lg:pl-64">
|
|
<!-- Top Navigation -->
|
|
<header class="sticky top-0 z-10 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b border-border">
|
|
<div class="flex items-center justify-between h-14 px-4">
|
|
<!-- Mobile menu button -->
|
|
<div class="flex items-center lg:hidden">
|
|
<button type="button" @click="sidebarOpen = true" class="p-2 text-muted-foreground rounded-md hover:bg-accent hover:text-accent-foreground">
|
|
<span class="sr-only">Open sidebar</span>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Page title -->
|
|
<div class="flex-1 px-2 mx-2">
|
|
<h1 class="text-lg font-semibold" x-text="getActiveTabTitle()"></h1>
|
|
</div>
|
|
|
|
<!-- Theme toggle and user profile -->
|
|
<div class="flex items-center space-x-2">
|
|
<button @click="toggleTheme()" class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring h-9 w-9 bg-transparent hover:bg-accent hover:text-accent-foreground">
|
|
<svg x-show="!isDarkMode" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-moon">
|
|
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
|
</svg>
|
|
<svg x-show="isDarkMode" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun">
|
|
<circle cx="12" cy="12" r="4"></circle>
|
|
<path d="M12 2v2"></path>
|
|
<path d="M12 20v2"></path>
|
|
<path d="m4.93 4.93 1.41 1.41"></path>
|
|
<path d="m17.66 17.66 1.41 1.41"></path>
|
|
<path d="M2 12h2"></path>
|
|
<path d="M20 12h2"></path>
|
|
<path d="m6.34 17.66-1.41 1.41"></path>
|
|
<path d="m19.07 4.93-1.41 1.41"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<button type="button" class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 w-9">
|
|
<i class="fas fa-user-circle text-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Page Content -->
|
|
<main class="p-6">
|
|
<!-- Dashboard Tab -->
|
|
<div x-show="activeTab === 'dashboard'" x-transition>
|
|
<!-- No Data Message -->
|
|
<div x-show="!hasDomainData" class="flex flex-col items-center justify-center p-8 rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="flex flex-col items-center max-w-md text-center">
|
|
<div class="rounded-full p-4 bg-muted mb-4">
|
|
<i class="fas fa-chart-line text-4xl text-muted-foreground"></i>
|
|
</div>
|
|
<h3 class="text-lg font-semibold mb-2">No DMARC reports have been uploaded yet</h3>
|
|
<p class="text-muted-foreground mb-4">Upload a report to see statistics and gain insights into your domain's email authentication.</p>
|
|
<button @click="activeTab = 'upload'" class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2">
|
|
<i class="fas fa-upload mr-2"></i> Upload Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dashboard Content (when data is available) -->
|
|
<div x-show="hasDomainData">
|
|
<!-- Stats Overview -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
<!-- Total Domains -->
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="p-6 flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<h3 class="tracking-tight text-sm font-medium">Total Domains</h3>
|
|
<div class="p-2 bg-primary/10 rounded-full">
|
|
<i class="fas fa-globe text-primary"></i>
|
|
</div>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div id="total-domains" class="text-2xl font-bold">0</div>
|
|
<p class="text-xs text-muted-foreground">Active domains being monitored</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total Emails -->
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="p-6 flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<h3 class="tracking-tight text-sm font-medium">Emails Analyzed</h3>
|
|
<div class="p-2 bg-blue-500/10 rounded-full">
|
|
<i class="fas fa-envelope text-blue-500"></i>
|
|
</div>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div id="total-emails" class="text-2xl font-bold">0</div>
|
|
<p class="text-xs text-muted-foreground">Total emails processed</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pass Rate -->
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="p-6 flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<h3 class="tracking-tight text-sm font-medium">Pass Rate</h3>
|
|
<div class="p-2 bg-green-500/10 rounded-full">
|
|
<i class="fas fa-check-circle text-green-500"></i>
|
|
</div>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div id="overall-pass-rate" class="text-2xl font-bold text-green-500">0%</div>
|
|
<p class="text-xs text-muted-foreground">Overall DMARC compliance</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reports Processed -->
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="p-6 flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<h3 class="tracking-tight text-sm font-medium">Reports Processed</h3>
|
|
<div class="p-2 bg-purple-500/10 rounded-full">
|
|
<i class="fas fa-file-alt text-purple-500"></i>
|
|
</div>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div id="reports-processed" class="text-2xl font-bold">0</div>
|
|
<p class="text-xs text-muted-foreground">DMARC reports received</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Domain Compliance Table -->
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold leading-none tracking-tight">Domain Compliance</h3>
|
|
<button @click="fetchDomainSummary" class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 px-3">
|
|
<i class="fas fa-sync-alt mr-2"></i> Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-0">
|
|
<div class="w-full overflow-auto">
|
|
<table class="w-full caption-bottom text-sm">
|
|
<thead class="[&_tr]:border-b">
|
|
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
<th class="h-10 px-4 text-left align-middle font-medium text-muted-foreground">Domain</th>
|
|
<th class="h-10 px-4 text-left align-middle font-medium text-muted-foreground">Emails</th>
|
|
<th class="h-10 px-4 text-left align-middle font-medium text-muted-foreground">Pass Rate</th>
|
|
<th class="h-10 px-4 text-left align-middle font-medium text-muted-foreground">Failed</th>
|
|
<th class="h-10 px-4 text-left align-middle font-medium text-muted-foreground">Reports</th>
|
|
<th class="h-10 px-4 text-right align-middle font-medium text-muted-foreground">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="domains-table-body" class="[&_tr:last-child]:border-0">
|
|
<!-- Domains will be added here dynamically -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Tab -->
|
|
<div x-show="activeTab === 'upload'" x-transition>
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<h3 class="text-lg font-semibold leading-none tracking-tight">Upload DMARC Reports</h3>
|
|
<p class="text-sm text-muted-foreground">Upload your DMARC aggregate report files (XML, ZIP, or GZIP)</p>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<form id="upload-form" enctype="multipart/form-data">
|
|
<div class="flex flex-col items-center justify-center border-2 border-dashed border-border rounded-md p-6 text-center hover:bg-muted/50 transition-colors cursor-pointer relative">
|
|
<div class="mb-3 text-muted-foreground">
|
|
<i class="fas fa-cloud-upload-alt text-4xl"></i>
|
|
</div>
|
|
<p class="mb-2 text-sm">
|
|
<span class="font-semibold">Click to upload</span> or drag and drop
|
|
</p>
|
|
<p class="text-xs text-muted-foreground">
|
|
XML, ZIP, or GZIP files only
|
|
</p>
|
|
<input id="report-file" type="file" name="file" accept=".xml,.zip,.gz,.gzip" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
|
</div>
|
|
|
|
<div id="file-selected" class="hidden mt-4 p-3 bg-muted rounded-md">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
<i class="fas fa-file-code text-primary"></i>
|
|
<span id="file-name" class="text-sm font-medium"></span>
|
|
</div>
|
|
<button type="button" id="remove-file" class="text-muted-foreground hover:text-foreground">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-end">
|
|
<button type="submit" class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2">
|
|
<i class="fas fa-upload mr-2"></i> Upload Report
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<div id="upload-result" class="mt-4"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Domains Tab -->
|
|
<div x-show="activeTab === 'domains'" x-transition>
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold leading-none tracking-tight">Domain Management</h3>
|
|
<button @click="fetchDomainSummary" class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 px-3">
|
|
<i class="fas fa-sync-alt mr-2"></i> Refresh
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-muted-foreground">View and manage domains with DMARC reports</p>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<p id="domains-message" class="text-center text-muted-foreground py-6">Loading domains...</p>
|
|
<div id="domains-list" class="hidden">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<!-- Domain cards will be added here dynamically -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Domain Details Container -->
|
|
<div id="domain-details-container" x-show="activeTab === 'domain-details'" x-transition class="hidden">
|
|
<div class="mb-4">
|
|
<button id="back-to-summary" class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 px-3">
|
|
<i class="fas fa-arrow-left mr-2"></i> Back to Summary
|
|
</button>
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow mb-6">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<h3 id="domain-details-title" class="text-lg font-semibold leading-none tracking-tight">Domain Details</h3>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div id="domain-details-content">
|
|
<!-- Domain details will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reports Tab -->
|
|
<div x-show="activeTab === 'reports'" x-transition>
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<h3 class="text-lg font-semibold leading-none tracking-tight">Reports Summary</h3>
|
|
<p class="text-sm text-muted-foreground">Advanced report visualization and analysis</p>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div class="flex flex-col items-center justify-center p-8">
|
|
<div class="rounded-full p-3 bg-muted">
|
|
<i class="fas fa-chart-line text-2xl text-muted-foreground"></i>
|
|
</div>
|
|
<h3 class="mt-4 text-lg font-medium">Coming Soon</h3>
|
|
<p class="text-sm text-center text-muted-foreground mt-2 max-w-md">
|
|
Advanced report visualization and analysis features will be available in future milestones.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Tab -->
|
|
<div x-show="activeTab === 'settings'" x-transition>
|
|
<div class="rounded-lg border border-border bg-card text-card-foreground shadow">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<h3 class="text-lg font-semibold leading-none tracking-tight">Settings</h3>
|
|
<p class="text-sm text-muted-foreground">User preferences and system configuration</p>
|
|
</div>
|
|
<div class="p-6 pt-0">
|
|
<div class="flex flex-col items-center justify-center p-8">
|
|
<div class="rounded-full p-3 bg-muted">
|
|
<i class="fas fa-cog text-2xl text-muted-foreground"></i>
|
|
</div>
|
|
<h3 class="mt-4 text-lg font-medium">Coming Soon</h3>
|
|
<p class="text-sm text-center text-muted-foreground mt-2 max-w-md">
|
|
User preferences and system configuration will be available in future milestones.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function dashboardApp() {
|
|
return {
|
|
activeTab: 'dashboard',
|
|
sidebarOpen: false,
|
|
isDarkMode: true,
|
|
hasDomainData: false, // Will be set true when data is loaded
|
|
|
|
init() {
|
|
// Check if we have any domains with data
|
|
this.fetchDomainSummary();
|
|
|
|
// Check system preferences for dark mode
|
|
const savedTheme = localStorage.getItem('dmarq-theme');
|
|
if (savedTheme) {
|
|
this.isDarkMode = savedTheme === 'dark';
|
|
this.applyTheme();
|
|
}
|
|
|
|
// Watch for tab changes to refresh data
|
|
this.$watch('activeTab', (tab) => {
|
|
if (tab === 'dashboard' || tab === 'domains') {
|
|
this.fetchDomainSummary();
|
|
}
|
|
});
|
|
|
|
// Listen for custom event to refresh data
|
|
document.addEventListener('dmarq:refresh-data', () => {
|
|
this.fetchDomainSummary();
|
|
});
|
|
},
|
|
|
|
getActiveTabTitle() {
|
|
switch (this.activeTab) {
|
|
case 'dashboard': return 'Dashboard';
|
|
case 'upload': return 'Upload Reports';
|
|
case 'domains': return 'Domain Management';
|
|
case 'reports': return 'Reports Analysis';
|
|
case 'settings': return 'Settings';
|
|
case 'domain-details': return 'Domain Details';
|
|
default: return 'DMARQ';
|
|
}
|
|
},
|
|
|
|
toggleTheme() {
|
|
this.isDarkMode = !this.isDarkMode;
|
|
localStorage.setItem('dmarq-theme', this.isDarkMode ? 'dark' : 'light');
|
|
this.applyTheme();
|
|
},
|
|
|
|
applyTheme() {
|
|
if (this.isDarkMode) {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
},
|
|
|
|
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);
|
|
this.populateDomainsList(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 = 'border-b transition-colors hover:bg-muted/50';
|
|
|
|
row.innerHTML = `
|
|
<td class="p-4 align-middle">
|
|
<div class="font-medium">${domain.domain_name}</div>
|
|
</td>
|
|
<td class="p-4 align-middle">${domain.total_emails || 0}</td>
|
|
<td class="p-4 align-middle">
|
|
<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="p-4 align-middle">${domain.failed_count || 0}</td>
|
|
<td class="p-4 align-middle">${domain.report_count || 0}</td>
|
|
<td class="p-4 align-middle text-right">
|
|
<button data-domain-id="${domain.id}" class="view-domain-details inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 px-3">
|
|
<i class="fas fa-chart-bar mr-1"></i> Details
|
|
</button>
|
|
</td>
|
|
`;
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
|
|
// Add event listeners to the view details buttons
|
|
document.querySelectorAll('.view-domain-details').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const domainId = button.getAttribute('data-domain-id');
|
|
this.viewDomainDetails(domainId);
|
|
});
|
|
});
|
|
},
|
|
|
|
populateDomainsList(domains) {
|
|
if (!domains || !domains.length) return;
|
|
|
|
const domainsMessage = document.getElementById('domains-message');
|
|
const domainsList = document.getElementById('domains-list');
|
|
const domainsGrid = domainsList.querySelector('.grid');
|
|
|
|
if (domainsMessage && domainsList && domainsGrid) {
|
|
// Hide loading message, show domain list
|
|
domainsMessage.classList.add('hidden');
|
|
domainsList.classList.remove('hidden');
|
|
|
|
domainsGrid.innerHTML = '';
|
|
|
|
domains.forEach(domain => {
|
|
const card = document.createElement('div');
|
|
card.className = 'rounded-lg border border-border bg-card text-card-foreground shadow';
|
|
|
|
card.innerHTML = `
|
|
<div class="p-6">
|
|
<h4 class="text-lg font-semibold mb-2">${domain.domain_name}</h4>
|
|
<div class="space-y-2">
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span class="text-muted-foreground">Emails:</span>
|
|
<span>${domain.total_emails || 0}</span>
|
|
</div>
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span class="text-muted-foreground">Pass Rate:</span>
|
|
<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>
|
|
</div>
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span class="text-muted-foreground">Failed:</span>
|
|
<span>${domain.failed_count || 0}</span>
|
|
</div>
|
|
<div class="flex justify-between items-center text-sm">
|
|
<span class="text-muted-foreground">Reports:</span>
|
|
<span>${domain.report_count || 0}</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 pt-4 border-t border-border flex justify-end">
|
|
<button data-domain-id="${domain.id}" class="view-domain-details inline-flex items-center justify-center rounded-md text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 px-3">
|
|
<i class="fas fa-chart-bar mr-1"></i> Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
domainsGrid.appendChild(card);
|
|
});
|
|
|
|
// Add event listeners to the view details buttons
|
|
domainsList.querySelectorAll('.view-domain-details').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const domainId = button.getAttribute('data-domain-id');
|
|
this.viewDomainDetails(domainId);
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
async viewDomainDetails(domainId) {
|
|
try {
|
|
const response = await fetch(`/api/v1/domains/${domainId}`);
|
|
const domain = await response.json();
|
|
|
|
// Update the domain details section
|
|
const detailsTitle = document.getElementById('domain-details-title');
|
|
const detailsContent = document.getElementById('domain-details-content');
|
|
|
|
if (detailsTitle) {
|
|
detailsTitle.textContent = `Domain: ${domain.domain_name}`;
|
|
}
|
|
|
|
if (detailsContent) {
|
|
// Populate the details content here
|
|
detailsContent.innerHTML = `
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div>
|
|
<h4 class="text-sm font-medium mb-2">Domain Information</h4>
|
|
<dl class="grid grid-cols-[100px_1fr] gap-1">
|
|
<dt class="text-xs text-muted-foreground">Name:</dt>
|
|
<dd class="text-sm">${domain.domain_name}</dd>
|
|
<dt class="text-xs text-muted-foreground">Reports:</dt>
|
|
<dd class="text-sm">${domain.report_count || 0}</dd>
|
|
<dt class="text-xs text-muted-foreground">Emails:</dt>
|
|
<dd class="text-sm">${domain.total_emails || 0}</dd>
|
|
</dl>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium mb-2">Compliance Summary</h4>
|
|
<dl class="grid grid-cols-[100px_1fr] gap-1">
|
|
<dt class="text-xs text-muted-foreground">Pass Rate:</dt>
|
|
<dd class="text-sm">${domain.pass_rate || 0}%</dd>
|
|
<dt class="text-xs text-muted-foreground">Failed:</dt>
|
|
<dd class="text-sm">${domain.failed_count || 0}</dd>
|
|
<dt class="text-xs text-muted-foreground">DKIM:</dt>
|
|
<dd class="text-sm">${domain.dkim_pass_count || 0} passed</dd>
|
|
<dt class="text-xs text-muted-foreground">SPF:</dt>
|
|
<dd class="text-sm">${domain.spf_pass_count || 0} passed</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h4 class="text-sm font-medium mb-2">Recent Reports</h4>
|
|
<div class="border rounded-md overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-muted">
|
|
<tr>
|
|
<th class="p-2 text-left font-medium text-muted-foreground">Report ID</th>
|
|
<th class="p-2 text-left font-medium text-muted-foreground">Date</th>
|
|
<th class="p-2 text-left font-medium text-muted-foreground">Source</th>
|
|
<th class="p-2 text-left font-medium text-muted-foreground">Emails</th>
|
|
<th class="p-2 text-left font-medium text-muted-foreground">Pass Rate</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr class="border-t">
|
|
<td colspan="5" class="p-4 text-center text-muted-foreground">
|
|
Report details will be available in future milestones.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show the domain details container and hide others
|
|
this.activeTab = 'domain-details';
|
|
document.getElementById('domain-details-container').classList.remove('hidden');
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching domain details:', error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle file upload preview
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const fileInput = document.getElementById('report-file');
|
|
const fileSelected = document.getElementById('file-selected');
|
|
const fileName = document.getElementById('file-name');
|
|
const removeFile = document.getElementById('remove-file');
|
|
const uploadForm = document.getElementById('upload-form');
|
|
const uploadResult = document.getElementById('upload-result');
|
|
const backToSummary = document.getElementById('back-to-summary');
|
|
|
|
if (fileInput && fileSelected && fileName && removeFile) {
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
fileName.textContent = e.target.files[0].name;
|
|
fileSelected.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
removeFile.addEventListener('click', () => {
|
|
fileInput.value = '';
|
|
fileSelected.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
if (uploadForm) {
|
|
uploadForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (!fileInput.files.length) {
|
|
uploadResult.innerHTML = `
|
|
<div class="p-3 bg-destructive/20 text-destructive rounded-md">
|
|
Please select a file to upload
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
uploadResult.innerHTML = `
|
|
<div class="p-3 bg-muted rounded-md flex items-center">
|
|
<i class="fas fa-spinner fa-spin mr-2"></i>
|
|
<span>Uploading and processing report...</span>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/reports/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
uploadResult.innerHTML = `
|
|
<div class="p-3 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 rounded-md">
|
|
<p><i class="fas fa-check-circle mr-2"></i> Report uploaded successfully</p>
|
|
<p class="text-xs mt-1">Processed ${data.processed_records || 0} records for domain ${data.domain || ''}</p>
|
|
</div>
|
|
`;
|
|
fileInput.value = '';
|
|
fileSelected.classList.add('hidden');
|
|
|
|
// Refresh the dashboard data after successful upload
|
|
// Use a custom event to communicate with the Alpine component
|
|
document.dispatchEvent(new CustomEvent('dmarq:refresh-data'));
|
|
} else {
|
|
throw new Error(data.detail || 'Upload failed');
|
|
}
|
|
} catch (error) {
|
|
uploadResult.innerHTML = `
|
|
<div class="p-3 bg-destructive/20 text-destructive rounded-md">
|
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
|
${error.message || 'An error occurred during upload'}
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Back button in domain details view
|
|
if (backToSummary) {
|
|
backToSummary.addEventListener('click', () => {
|
|
document.getElementById('domain-details-container').classList.add('hidden');
|
|
const app = document.querySelector('[x-data="dashboardApp()"]').__x.$data;
|
|
app.activeTab = 'domains';
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |