Remove checks
This commit is contained in:
parent
954a9d705e
commit
eef6f4cf57
28 changed files with 1635 additions and 3824 deletions
210
web/src/lib/components/AuthenticationCard.svelte
Normal file
210
web/src/lib/components/AuthenticationCard.svelte
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<script lang="ts">
|
||||
import type { Authentication, ReportSummary } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
authentication: Authentication;
|
||||
authenticationScore?: number;
|
||||
}
|
||||
|
||||
let { authentication, authenticationScore }: Props = $props();
|
||||
|
||||
function getAuthResultClass(result: string): string {
|
||||
switch (result) {
|
||||
case "pass":
|
||||
return "text-success";
|
||||
case "fail":
|
||||
case "missing":
|
||||
return "text-danger";
|
||||
case "softfail":
|
||||
case "neutral":
|
||||
return "text-warning";
|
||||
default:
|
||||
return "text-muted";
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthResultIcon(result: string): string {
|
||||
switch (result) {
|
||||
case "pass":
|
||||
return "bi-check-circle-fill";
|
||||
case "fail":
|
||||
return "bi-x-circle-fill";
|
||||
case "softfail":
|
||||
case "neutral":
|
||||
return "bi-exclamation-circle-fill";
|
||||
case "missing":
|
||||
return "bi-dash-circle-fill";
|
||||
default:
|
||||
return "bi-question-circle";
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthResultText(result: string): string {
|
||||
switch (result) {
|
||||
case "missing":
|
||||
return "Not configured";
|
||||
default:
|
||||
return result;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
Authentication
|
||||
</span>
|
||||
{#if authenticationScore !== undefined}
|
||||
<span class="badge bg-secondary">
|
||||
{authenticationScore}%
|
||||
</span>
|
||||
{/if}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1">
|
||||
<!-- SPF (Required) -->
|
||||
<div class="col mb-3">
|
||||
<div class="d-flex align-items-start">
|
||||
{#if authentication.spf}
|
||||
<i class="bi {getAuthResultIcon(authentication.spf.result)} {getAuthResultClass(authentication.spf.result)} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>SPF</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.spf.result)}">
|
||||
{authentication.spf.result}
|
||||
</span>
|
||||
{#if authentication.spf.domain}
|
||||
<div class="small">
|
||||
<strong>Domain:</strong>
|
||||
<span class="text-muted">{authentication.spf.domain}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if authentication.spf.details}
|
||||
<pre class="p-2 mb-0 bg-light text-muted small" style="white-space: pre-wrap">{authentication.spf.details}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>SPF</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}">
|
||||
{getAuthResultText('missing')}
|
||||
</span>
|
||||
<div class="text-muted small">SPF record is required for proper email authentication</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DKIM (Required) -->
|
||||
<div class="col mb-3">
|
||||
<div class="d-flex align-items-start">
|
||||
{#if authentication.dkim && authentication.dkim.length > 0}
|
||||
<i class="bi {getAuthResultIcon(authentication.dkim[0].result)} {getAuthResultClass(authentication.dkim[0].result)} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>DKIM</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dkim[0].result)}">
|
||||
{authentication.dkim[0].result}
|
||||
</span>
|
||||
{#if authentication.dkim[0].domain}
|
||||
<div class="text-muted small">{authentication.dkim[0].domain}</div>
|
||||
{/if}
|
||||
{#if authentication.dkim[0].selector}
|
||||
<div class="text-muted small">Selector: {authentication.dkim[0].selector}</div>
|
||||
{/if}
|
||||
{#if authentication.dkim.details}
|
||||
<pre class="p-2 mb-0 bg-light text-muted small" style="white-space: pre-wrap">{authentication.dkim.details}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>DKIM</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}">
|
||||
{getAuthResultText('missing')}
|
||||
</span>
|
||||
<div class="text-muted small">DKIM signature is required for proper email authentication</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DMARC (Required) -->
|
||||
<div class="col mb-3">
|
||||
<div class="d-flex align-items-start">
|
||||
{#if authentication.dmarc}
|
||||
<i class="bi {getAuthResultIcon(authentication.dmarc.result)} {getAuthResultClass(authentication.dmarc.result)} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>DMARC</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dmarc.result)}">
|
||||
{authentication.dmarc.result}
|
||||
</span>
|
||||
{#if authentication.dmarc.details}
|
||||
<pre class="p-2 mb-0 bg-light text-muted small" style="white-space: pre-wrap">{authentication.dmarc.details}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>DMARC</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}">
|
||||
{getAuthResultText('missing')}
|
||||
</span>
|
||||
<div class="text-muted small">DMARC policy is required for proper email authentication</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BIMI (Optional) -->
|
||||
<div class="col mb-3">
|
||||
<div class="d-flex align-items-start">
|
||||
{#if authentication.bimi}
|
||||
<i class="bi {getAuthResultIcon(authentication.bimi.result)} {getAuthResultClass(authentication.bimi.result)} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>BIMI</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result)}">
|
||||
{authentication.bimi.result}
|
||||
</span>
|
||||
{#if authentication.bimi.details}
|
||||
<pre class="p-2 mb-0 bg-light text-muted small" style="white-space: pre-wrap">{authentication.bimi.details}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<i class="bi bi-info-circle text-muted me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>BIMI</strong>
|
||||
<span class="text-uppercase ms-2 text-muted">
|
||||
Optional
|
||||
</span>
|
||||
<div class="text-muted small">Brand Indicators for Message Identification (optional enhancement)</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ARC (Optional) -->
|
||||
{#if authentication.arc}
|
||||
<div class="col mb-3">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi {getAuthResultIcon(authentication.arc.result)} {getAuthResultClass(authentication.arc.result)} me-2 fs-5"></i>
|
||||
<div>
|
||||
<strong>ARC</strong>
|
||||
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result)}">
|
||||
{authentication.arc.result}
|
||||
</span>
|
||||
{#if authentication.arc.chain_length}
|
||||
<div class="text-muted small">Chain length: {authentication.arc.chain_length}</div>
|
||||
{/if}
|
||||
{#if authentication.arc.details}
|
||||
<pre class="p-2 mb-0 bg-light text-muted small" style="white-space: pre-wrap">{authentication.arc.details}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
60
web/src/lib/components/BlacklistCard.svelte
Normal file
60
web/src/lib/components/BlacklistCard.svelte
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="ts">
|
||||
import type { RBLCheck } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
blacklists: Record<string, RBLCheck[]>;
|
||||
blacklistScore?: number;
|
||||
}
|
||||
|
||||
let { blacklists, blacklistScore }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-shield-exclamation me-2"></i>
|
||||
Blacklist Checks
|
||||
</span>
|
||||
{#if blacklistScore !== undefined}
|
||||
<span class="badge bg-secondary">
|
||||
{blacklistScore}%
|
||||
</span>
|
||||
{/if}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-lg-2">
|
||||
{#each Object.entries(blacklists) as [ip, checks]}
|
||||
<div class="col mb-3">
|
||||
<h6 class="text-muted">
|
||||
<i class="bi bi-hdd-network me-1"></i>
|
||||
{ip}
|
||||
</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RBL</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each checks as check}
|
||||
<tr>
|
||||
<td><code>{check.rbl}</code></td>
|
||||
<td title={check.response || '-'}>
|
||||
<span class="badge {check.listed ? 'bg-danger' : check.error ? 'bg-dark' : 'bg-success'}">
|
||||
{check.error ? 'Error' : (check.listed ? 'Listed' : 'Clean')}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Check } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
check: Check;
|
||||
}
|
||||
|
||||
let { check }: Props = $props();
|
||||
|
||||
function getCheckIcon(status: string): string {
|
||||
switch (status) {
|
||||
case "pass":
|
||||
return "bi-check-circle-fill text-success";
|
||||
case "fail":
|
||||
return "bi-x-circle-fill text-danger";
|
||||
case "warn":
|
||||
return "bi-exclamation-triangle-fill text-warning";
|
||||
case "info":
|
||||
return "bi-info-circle-fill text-info";
|
||||
default:
|
||||
return "bi-question-circle-fill text-secondary";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="fs-4">
|
||||
<i class={getCheckIcon(check.status)}></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<h5 class="fw-bold mb-1">{check.name}</h5>
|
||||
<span class="badge bg-light text-dark">{check.score}%</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 mb-2">{check.message}</p>
|
||||
|
||||
{#if check.advice}
|
||||
<div class="alert alert-light border mb-2" role="alert">
|
||||
<i class="bi bi-lightbulb me-2"></i>
|
||||
<strong>Recommendation:</strong>
|
||||
{check.advice}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if check.details}
|
||||
<details class="small text-muted">
|
||||
<summary class="cursor-pointer">Technical Details</summary>
|
||||
<pre class="mt-2 mb-0 small bg-light p-2 rounded" style="white-space: pre-wrap;">{check.details}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
</style>
|
||||
165
web/src/lib/components/ContentAnalysisCard.svelte
Normal file
165
web/src/lib/components/ContentAnalysisCard.svelte
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts">
|
||||
import type { ContentAnalysis } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
contentAnalysis: ContentAnalysis;
|
||||
contentScore?: number;
|
||||
}
|
||||
|
||||
let { contentAnalysis, contentScore }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-file-text me-2"></i>
|
||||
Content Analysis
|
||||
</span>
|
||||
{#if contentScore !== undefined}
|
||||
<span class="badge bg-secondary">
|
||||
{contentScore}%
|
||||
</span>
|
||||
{/if}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi {contentAnalysis.has_html ? 'bi-check-circle text-success' : 'bi-x-circle text-muted'} me-2"></i>
|
||||
<span>HTML Part</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi {contentAnalysis.has_plaintext ? 'bi-check-circle text-success' : 'bi-x-circle text-muted'} me-2"></i>
|
||||
<span>Plaintext Part</span>
|
||||
</div>
|
||||
{#if typeof contentAnalysis.has_unsubscribe_link === 'boolean'}
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi {contentAnalysis.has_unsubscribe_link ? 'bi-check-circle text-success' : 'bi-x-circle text-warning'} me-2"></i>
|
||||
<span>Unsubscribe Link</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{#if contentAnalysis.text_to_image_ratio !== undefined}
|
||||
<div class="mb-2">
|
||||
<strong>Text to Image Ratio:</strong>
|
||||
<span class="ms-2">{contentAnalysis.text_to_image_ratio.toFixed(2)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if contentAnalysis.unsubscribe_methods && contentAnalysis.unsubscribe_methods.length > 0}
|
||||
<div class="mb-2">
|
||||
<strong>Unsubscribe Methods:</strong>
|
||||
<div class="mt-1">
|
||||
{#each contentAnalysis.unsubscribe_methods as method}
|
||||
<span class="badge bg-info me-1">{method}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if contentAnalysis.html_issues && contentAnalysis.html_issues.length > 0}
|
||||
<div class="mt-3">
|
||||
<h6>Content Issues</h6>
|
||||
{#each contentAnalysis.html_issues as issue}
|
||||
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>{issue.type}</strong>
|
||||
<div class="small">{issue.message}</div>
|
||||
{#if issue.location}
|
||||
<div class="small text-muted">{issue.location}</div>
|
||||
{/if}
|
||||
{#if issue.advice}
|
||||
<div class="small mt-1">
|
||||
<i class="bi bi-lightbulb me-1"></i>
|
||||
{issue.advice}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="badge bg-secondary">{issue.severity}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if contentAnalysis.links && contentAnalysis.links.length > 0}
|
||||
<div class="mt-3">
|
||||
<h6>Links ({contentAnalysis.links.length})</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<th>Status</th>
|
||||
<th>HTTP Code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each contentAnalysis.links as link}
|
||||
<tr>
|
||||
<td>
|
||||
<small class="text-break">{link.url}</small>
|
||||
{#if link.is_shortened}
|
||||
<span class="badge bg-warning ms-1">Shortened</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {link.status === 'valid' ? 'bg-success' : link.status === 'broken' ? 'bg-danger' : 'bg-warning'}">
|
||||
{link.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>{link.http_code || '-'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if contentAnalysis.images && contentAnalysis.images.length > 0}
|
||||
<div class="mt-3">
|
||||
<h6>Images ({contentAnalysis.images.length})</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Alt Text</th>
|
||||
<th>Tracking</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each contentAnalysis.images as image}
|
||||
<tr>
|
||||
<td><small class="text-break">{image.src || '-'}</small></td>
|
||||
<td>
|
||||
{#if image.has_alt}
|
||||
<i class="bi bi-check-circle text-success me-1"></i>
|
||||
<small>{image.alt_text || 'Present'}</small>
|
||||
{:else}
|
||||
<i class="bi bi-x-circle text-warning me-1"></i>
|
||||
<small class="text-muted">Missing</small>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if image.is_tracking_pixel}
|
||||
<span class="badge bg-info">Tracking Pixel</span>
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
46
web/src/lib/components/DnsRecordsCard.svelte
Normal file
46
web/src/lib/components/DnsRecordsCard.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import type { DNSRecord } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
dnsRecords: DNSRecord[];
|
||||
}
|
||||
|
||||
let { dnsRecords }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-diagram-3 me-2"></i>
|
||||
DNS Records
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each dnsRecords as record}
|
||||
<tr>
|
||||
<td><code>{record.domain}</code></td>
|
||||
<td><span class="badge bg-secondary">{record.record_type}</span></td>
|
||||
<td>
|
||||
<span class="badge {record.status === 'found' ? 'bg-success' : record.status === 'missing' ? 'bg-danger' : 'bg-warning'}">
|
||||
{record.status}
|
||||
</span>
|
||||
</td>
|
||||
<td><small class="text-muted">{record.value || '-'}</small></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
169
web/src/lib/components/HeaderAnalysisCard.svelte
Normal file
169
web/src/lib/components/HeaderAnalysisCard.svelte
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<script lang="ts">
|
||||
import type { HeaderAnalysis } from "$lib/api/types.gen";
|
||||
|
||||
interface Props {
|
||||
headerAnalysis: HeaderAnalysis;
|
||||
headerScore?: number;
|
||||
}
|
||||
|
||||
let { headerAnalysis, headerScore }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-list-ul me-2"></i>
|
||||
Header Analysis
|
||||
</span>
|
||||
{#if headerScore !== undefined}
|
||||
<span class="badge bg-secondary">
|
||||
{headerScore}%
|
||||
</span>
|
||||
{/if}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{#if headerAnalysis.issues && headerAnalysis.issues.length > 0}
|
||||
<div class="mb-3">
|
||||
<h6>Issues</h6>
|
||||
{#each headerAnalysis.issues as issue}
|
||||
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>{issue.header}</strong>
|
||||
<div class="small">{issue.message}</div>
|
||||
{#if issue.advice}
|
||||
<div class="small mt-1">
|
||||
<i class="bi bi-lightbulb me-1"></i>
|
||||
{issue.advice}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="badge bg-secondary">{issue.severity}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if headerAnalysis.domain_alignment}
|
||||
<div class="mb-3">
|
||||
<h6>Domain Alignment</h6>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted">From Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted">Return-Path Domain</small>
|
||||
<div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted">Aligned</small>
|
||||
<div>
|
||||
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'} me-1"></i>
|
||||
{headerAnalysis.domain_alignment.aligned ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
|
||||
<div class="mt-2">
|
||||
{#each headerAnalysis.domain_alignment.issues as issue}
|
||||
<div class="text-warning small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
{issue}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if headerAnalysis.headers && Object.keys(headerAnalysis.headers).length > 0}
|
||||
<div class="mt-3">
|
||||
<h6>Headers</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Header</th>
|
||||
<th>Present</th>
|
||||
<th>Valid</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(headerAnalysis.headers) as [name, check]}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{name}</code>
|
||||
{#if check.importance}
|
||||
<span class="badge bg-{check.importance === 'required' ? 'danger' : check.importance === 'recommended' ? 'warning' : 'secondary'} ms-1">
|
||||
{check.importance}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<i class="bi {check.present ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
|
||||
</td>
|
||||
<td>
|
||||
{#if check.valid !== undefined}
|
||||
<i class="bi {check.valid ? 'bi-check-circle text-success' : 'bi-x-circle text-warning'}"></i>
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted text-break">{check.value || '-'}</small>
|
||||
{#if check.issues && check.issues.length > 0}
|
||||
{#each check.issues as issue}
|
||||
<div class="text-warning small">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
{issue}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if headerAnalysis.received_chain && headerAnalysis.received_chain.length > 0}
|
||||
<div class="mt-3">
|
||||
<h6>Email Path (Received Chain)</h6>
|
||||
<div class="list-group">
|
||||
{#each headerAnalysis.received_chain as hop, i}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">
|
||||
<span class="badge bg-primary me-2">{i + 1}</span>
|
||||
{hop.from || 'Unknown'} → {hop.by || 'Unknown'}
|
||||
</h6>
|
||||
<small class="text-muted">{hop.timestamp || '-'}</small>
|
||||
</div>
|
||||
{#if hop.with || hop.id}
|
||||
<p class="mb-1 small">
|
||||
{#if hop.with}
|
||||
<span class="text-muted">Protocol:</span> <code>{hop.with}</code>
|
||||
{/if}
|
||||
{#if hop.id}
|
||||
<span class="text-muted ms-3">ID:</span> <code>{hop.id}</code>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -50,19 +50,6 @@
|
|||
<small class="text-muted d-block">Authentication</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-2 bg-light rounded text-center">
|
||||
<strong
|
||||
class="fs-2"
|
||||
class:text-success={summary.spam_score >= 100}
|
||||
class:text-warning={summary.spam_score < 100 && summary.spam_score >= 50}
|
||||
class:text-danger={summary.spam_score < 50}
|
||||
>
|
||||
{summary.spam_score}%
|
||||
</strong>
|
||||
<small class="text-muted d-block">Spam Score</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-2 bg-light rounded text-center">
|
||||
<strong
|
||||
|
|
@ -77,20 +64,6 @@
|
|||
<small class="text-muted d-block">Blacklists</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-2 bg-light rounded text-center">
|
||||
<strong
|
||||
class="fs-2"
|
||||
class:text-success={summary.content_score >= 100}
|
||||
class:text-warning={summary.content_score < 100 &&
|
||||
summary.content_score >= 50}
|
||||
class:text-danger={summary.content_score < 50}
|
||||
>
|
||||
{summary.content_score}%
|
||||
</strong>
|
||||
<small class="text-muted d-block">Content</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-2 bg-light rounded text-center">
|
||||
<strong
|
||||
|
|
@ -105,6 +78,33 @@
|
|||
<small class="text-muted d-block">Headers</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-2 bg-light rounded text-center">
|
||||
<strong
|
||||
class="fs-2"
|
||||
class:text-success={summary.spam_score >= 100}
|
||||
class:text-warning={summary.spam_score < 100 && summary.spam_score >= 50}
|
||||
class:text-danger={summary.spam_score < 50}
|
||||
>
|
||||
{summary.spam_score}%
|
||||
</strong>
|
||||
<small class="text-muted d-block">Spam Score</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg">
|
||||
<div class="p-2 bg-light rounded text-center">
|
||||
<strong
|
||||
class="fs-2"
|
||||
class:text-success={summary.content_score >= 100}
|
||||
class:text-warning={summary.content_score < 100 &&
|
||||
summary.content_score >= 50}
|
||||
class:text-danger={summary.content_score < 50}
|
||||
>
|
||||
{summary.content_score}%
|
||||
</strong>
|
||||
<small class="text-muted d-block">Content</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
export { default as FeatureCard } from "./FeatureCard.svelte";
|
||||
export { default as HowItWorksStep } from "./HowItWorksStep.svelte";
|
||||
export { default as ScoreCard } from "./ScoreCard.svelte";
|
||||
export { default as CheckCard } from "./CheckCard.svelte";
|
||||
export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
|
||||
export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte";
|
||||
export { default as PendingState } from "./PendingState.svelte";
|
||||
export { default as AuthenticationCard } from "./AuthenticationCard.svelte";
|
||||
export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte";
|
||||
export { default as BlacklistCard } from "./BlacklistCard.svelte";
|
||||
export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte";
|
||||
export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,16 @@
|
|||
import { page } from "$app/state";
|
||||
import { getTest, getReport, reanalyzeReport } from "$lib/api";
|
||||
import type { Test, Report } from "$lib/api/types.gen";
|
||||
import { ScoreCard, CheckCard, SpamAssassinCard, PendingState } from "$lib/components";
|
||||
import {
|
||||
ScoreCard,
|
||||
SpamAssassinCard,
|
||||
PendingState,
|
||||
AuthenticationCard,
|
||||
DnsRecordsCard,
|
||||
BlacklistCard,
|
||||
ContentAnalysisCard,
|
||||
HeaderAnalysisCard
|
||||
} from "$lib/components";
|
||||
|
||||
let testId = $derived(page.params.test);
|
||||
let test = $state<Test | null>(null);
|
||||
|
|
@ -15,20 +24,6 @@
|
|||
let nextfetch = $state(23);
|
||||
let nbfetch = $state(0);
|
||||
|
||||
// Group checks by category
|
||||
let groupedChecks = $derived(() => {
|
||||
if (!report) return { };
|
||||
|
||||
const groups: Record<string, typeof report.checks> = { };
|
||||
for (const check of report.checks) {
|
||||
if (!groups[check.category]) {
|
||||
groups[check.category] = [];
|
||||
}
|
||||
groups[check.category].push(check);
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
async function fetchTest() {
|
||||
if (nbfetch > 0) {
|
||||
nextfetch = Math.max(nextfetch, Math.floor(3 + nbfetch * 0.5));
|
||||
|
|
@ -86,29 +81,6 @@
|
|||
stopPolling();
|
||||
});
|
||||
|
||||
function getCategoryIcon(category: string): string {
|
||||
switch (category) {
|
||||
case "authentication":
|
||||
return "bi-shield-check";
|
||||
case "dns":
|
||||
return "bi-diagram-3";
|
||||
case "content":
|
||||
return "bi-file-text";
|
||||
case "blacklist":
|
||||
return "bi-shield-exclamation";
|
||||
case "headers":
|
||||
return "bi-list-ul";
|
||||
case "spam":
|
||||
return "bi-filter";
|
||||
default:
|
||||
return "bi-question-circle";
|
||||
}
|
||||
}
|
||||
|
||||
function getCategoryScore(checks: typeof report.checks): number {
|
||||
return Math.round(checks.reduce((sum, check) => sum + check.score, 0) / checks.filter((c) => c.status != "info").length);
|
||||
}
|
||||
|
||||
function getScoreColorClass(percentage: number): string {
|
||||
if (percentage >= 80) return "text-success";
|
||||
if (percentage >= 50) return "text-warning";
|
||||
|
|
@ -166,45 +138,78 @@
|
|||
<!-- Results State -->
|
||||
<div class="fade-in">
|
||||
<!-- Score Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="row mb-4" id="score">
|
||||
<div class="col-12">
|
||||
<ScoreCard grade={report.grade} score={report.score} summary={report.summary} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Checks -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h3 class="fw-bold mb-3">Detailed Checks</h3>
|
||||
{#each Object.entries(groupedChecks()) as [category, checks]}
|
||||
{@const categoryScore = getCategoryScore(checks)}
|
||||
<div class="category-section mb-4">
|
||||
<h4 class="category-title text-capitalize mb-3 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi {getCategoryIcon(category)} me-2"></i>
|
||||
{category}
|
||||
</span>
|
||||
<span class="category-score {getScoreColorClass(categoryScore)}">
|
||||
{categoryScore}%
|
||||
</span>
|
||||
</h4>
|
||||
{#each checks as check}
|
||||
<CheckCard {check} />
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- DNS Records -->
|
||||
{#if report.dns_records && report.dns_records.length > 0}
|
||||
<div class="row mb-4" id="dns">
|
||||
<div class="col-12">
|
||||
<DnsRecordsCard dnsRecords={report.dns_records} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Authentication Results -->
|
||||
{#if report.authentication}
|
||||
<div class="row mb-4" id="authentication">
|
||||
<div class="col-12">
|
||||
<AuthenticationCard
|
||||
authentication={report.authentication}
|
||||
authenticationScore={report.summary?.authentication_score}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Blacklist Checks -->
|
||||
{#if report.blacklists && Object.keys(report.blacklists).length > 0}
|
||||
<div class="row mb-4" id="blacklist">
|
||||
<div class="col-12">
|
||||
<BlacklistCard
|
||||
blacklists={report.blacklists}
|
||||
blacklistScore={report.summary?.blacklist_score}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Header Analysis -->
|
||||
{#if report.header_analysis}
|
||||
<div class="row mb-4" id="header">
|
||||
<div class="col-12">
|
||||
<HeaderAnalysisCard
|
||||
headerAnalysis={report.header_analysis}
|
||||
headerScore={report.summary?.header_score}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Additional Information -->
|
||||
{#if report.spamassassin}
|
||||
<div class="row mb-4">
|
||||
<div class="row mb-4" id="spam">
|
||||
<div class="col-12">
|
||||
<SpamAssassinCard spamassassin={report.spamassassin} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Content Analysis -->
|
||||
{#if report.content_analysis}
|
||||
<div class="row mb-4" id="content">
|
||||
<div class="col-12">
|
||||
<ContentAnalysisCard
|
||||
contentAnalysis={report.content_analysis}
|
||||
contentScore={report.summary?.content_score}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue