Fix typescript/svelte checks

This commit is contained in:
nemunaire 2025-10-24 17:20:35 +07:00
commit 474f25007b
17 changed files with 199 additions and 155 deletions

View file

@ -1,13 +1,13 @@
<script lang="ts">
import type { Authentication, DNSResults, ReportSummary } from "$lib/api/types.gen";
import type { AuthenticationResults, DnsResults } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte";
interface Props {
authentication: Authentication;
authentication: AuthenticationResults;
authenticationGrade?: string;
authenticationScore?: number;
dnsResults?: DNSResults;
dnsResults?: DnsResults;
}
let { authentication, authenticationGrade, authenticationScore, dnsResults }: Props = $props();
@ -132,10 +132,10 @@
{/if}
</div>
{:else}
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div>
<strong>SPF</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}">
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')}
</span>
<div class="text-muted small">SPF record is required for proper email authentication</div>
@ -171,10 +171,10 @@
{/if}
</div>
{:else}
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div>
<strong>DKIM</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}">
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')}
</span>
<div class="text-muted small">DKIM signature is required for proper email authentication</div>
@ -214,7 +214,7 @@
{/if}
<!-- X-Aligned-From (Disabled) -->
{#if false && authentication.x_aligned_from}
{#if authentication.x_aligned_from}
<div class="list-group-item" id="authentication-x-aligned-from">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.x_aligned_from.result, false)} {getAuthResultClass(authentication.x_aligned_from.result, false)} me-2 fs-5"></i>
@ -253,7 +253,7 @@
<span class="text-muted">{authentication.dmarc.domain}</span>
</div>
{/if}
{#snippet DMARCPolicy(policy)}
{#snippet DMARCPolicy(policy: string)}
<div class="small">
<strong>Policy:</strong>
<span
@ -268,10 +268,10 @@
</div>
{/snippet}
{#if authentication.dmarc.result != "none"}
{#if authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
{#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
{@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")}
{@render DMARCPolicy(policy)}
{:else if authentication.dmarc.domain}
{:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy}
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
{/if}
{/if}
@ -280,10 +280,10 @@
{/if}
</div>
{:else}
<i class="bi {getAuthResultIcon('missing')} {getAuthResultClass('missing')} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
<div>
<strong>DMARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass('missing')}">
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
{getAuthResultText('missing')}
</span>
<div class="text-muted small">DMARC policy is required for proper email authentication</div>
@ -296,10 +296,10 @@
<div class="list-group-item" id="authentication-bimi">
<div class="d-flex align-items-start">
{#if authentication.bimi && authentication.bimi.result != "none"}
<i class="bi {getAuthResultIcon(authentication.bimi.result)} {getAuthResultClass(authentication.bimi.result)} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon(authentication.bimi.result, false)} {getAuthResultClass(authentication.bimi.result, false)} me-2 fs-5"></i>
<div>
<strong>BIMI</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result)}">
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result, false)}">
{authentication.bimi.result}
</span>
{#if authentication.bimi.details}
@ -335,10 +335,10 @@
{#if authentication.arc}
<div class="list-group-item" id="authentication-arc">
<div class="d-flex align-items-start">
<i class="bi {getAuthResultIcon(authentication.arc.result)} {getAuthResultClass(authentication.arc.result)} me-2 fs-5"></i>
<i class="bi {getAuthResultIcon(authentication.arc.result, false)} {getAuthResultClass(authentication.arc.result, false)} me-2 fs-5"></i>
<div>
<strong>ARC</strong>
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result)}">
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result, false)}">
{authentication.arc.result}
</span>
{#if authentication.arc.chain_length}

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { BIMIRecord } from "$lib/api/types.gen";
import type { BimiRecord } from "$lib/api/types.gen";
interface Props {
bimiRecord?: BIMIRecord;
bimiRecord?: BimiRecord;
}
let { bimiRecord }: Props = $props();

View file

@ -1,11 +1,11 @@
<script lang="ts">
import type { RBLCheck, ReceivedHop } from "$lib/api/types.gen";
import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte";
import EmailPathCard from "./EmailPathCard.svelte";
interface Props {
blacklists: Record<string, RBLCheck[]>;
blacklists: Record<string, BlacklistCheck[]>;
blacklistGrade?: string;
blacklistScore?: number;
receivedChain?: ReceivedHop[];

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { DKIMRecord } from "$lib/api/types.gen";
import type { DkimRecord } from "$lib/api/types.gen";
interface Props {
dkimRecords?: DKIMRecord[];
dkimRecords?: DkimRecord[];
}
let { dkimRecords }: Props = $props();

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { DMARCRecord } from "$lib/api/types.gen";
import type { DmarcRecord } from "$lib/api/types.gen";
interface Props {
dmarcRecord?: DMARCRecord;
dmarcRecord?: DmarcRecord;
}
let { dmarcRecord }: Props = $props();

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { DomainAlignment, DNSResults, ReceivedHop } from "$lib/api/types.gen";
import type { DomainAlignment, DnsResults, ReceivedHop } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte";
import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
@ -12,7 +12,7 @@
interface Props {
domainAlignment?: DomainAlignment;
dnsResults?: DNSResults;
dnsResults?: DnsResults;
dnsGrade?: string;
dnsScore?: number;
receivedChain?: ReceivedHop[];

View file

@ -1,7 +1,7 @@
<script lang="ts">
interface Props {
grade?: string;
score: number;
score?: number;
size?: "inline" | "small" | "medium" | "large";
}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import type { AuthResult, DMARCRecord, HeaderAnalysis } from "$lib/api/types.gen";
import type { AuthResult, DmarcRecord, HeaderAnalysis } from "$lib/api/types.gen";
import { getScoreColorClass } from "$lib/score";
import GradeDisplay from "./GradeDisplay.svelte";
interface Props {
dmarcRecord: DMARCRecord;
dmarcRecord?: DmarcRecord;
headerAnalysis: HeaderAnalysis;
headerGrade?: string;
headerScore?: number;
@ -62,7 +62,7 @@
<div class="card-header">
<h5 class="mb-0">
{#if xAlignedFrom}
<i class="bi {xAlignedFrom == "pass" ? 'bi-check-circle-fill text-success' : 'bi-x-circle-fill text-danger'}"></i>
<i class="bi {xAlignedFrom.result == "pass" ? 'bi-check-circle-fill text-success' : 'bi-x-circle-fill text-danger'}"></i>
{:else}
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill text-success' : headerAnalysis.domain_alignment.relaxed_aligned ? 'bi-check-circle text-info' : 'bi-x-circle-fill text-danger'}"></i>
{/if}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
import type { MXRecord } from "$lib/api/types.gen";
import type { ClassValue } from "svelte/elements";
import type { MxRecord } from "$lib/api/types.gen";
interface Props {
class: ClassValue;
mxRecords: MXRecord[];
mxRecords: MxRecord[];
title: string;
description?: string;
}

View file

@ -8,6 +8,8 @@
interface Props {
test: Test;
nbfetch: number;
nextfetch: number;
fetching?: boolean;
}

View file

@ -5,8 +5,8 @@
interface Props {
spamassassin: SpamAssassinResult;
spamGrade: string;
spamScore: number;
spamGrade?: string;
spamScore?: number;
}
let { spamassassin, spamGrade, spamScore }: Props = $props();

View file

@ -1,18 +1,18 @@
<script lang="ts">
import type { SPFRecord } from "$lib/api/types.gen";
import type { SpfRecord } from "$lib/api/types.gen";
interface Props {
spfRecords?: SPFRecord[];
spfRecords?: SpfRecord[];
}
let { spfRecords }: Props = $props();
// Compute overall validity
const spfIsValid = $derived(
spfRecords?.reduce((acc, r) => acc && r.valid, true) ?? false
);
const spfIsValid = $derived(spfRecords?.reduce((acc, r) => acc && r.valid, true) ?? false);
const spfCanBeImprove = $derived(
spfRecords.length > 0 && spfRecords.filter((r) => !r.record.includes(" redirect="))[0]?.all_qualifier != "-"
spfRecords &&
spfRecords.length > 0 &&
spfRecords.filter((r) => !r.record?.includes(" redirect="))[0]?.all_qualifier != "-",
);
</script>
@ -58,13 +58,13 @@
{#if spf.all_qualifier}
<div class="mb-2">
<strong>All Mechanism Policy:</strong>
{#if spf.all_qualifier === '-'}
{#if spf.all_qualifier === "-"}
<span class="badge bg-success">Strict (-all)</span>
{:else if spf.all_qualifier === '~'}
{:else if spf.all_qualifier === "~"}
<span class="badge bg-warning">Softfail (~all)</span>
{:else if spf.all_qualifier === '+'}
{:else if spf.all_qualifier === "+"}
<span class="badge bg-danger">Pass (+all)</span>
{:else if spf.all_qualifier === '?'}
{:else if spf.all_qualifier === "?"}
<span class="badge bg-warning">Neutral (?all)</span>
{/if}
{#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))}

View file

@ -5,8 +5,10 @@
interface TextSegment {
text: string;
highlight?: {
color: "good" | "warning" | "danger";
color?: "good" | "warning" | "danger";
bold?: boolean;
emphasis?: boolean;
monospace?: boolean;
};
link?: string;
}
@ -22,19 +24,19 @@
// Email sender information
const mailFrom = report.header_analysis?.headers?.from?.value || "an unknown sender";
const hasDkim = report.authentication?.dkim && report.authentication.dkim.length > 0;
const dkimPassed = hasDkim && report.authentication.dkim.some(d => d.result === "pass");
const hasDkim = report.authentication?.dkim && report.authentication?.dkim.length > 0;
const dkimPassed = hasDkim && report.authentication?.dkim?.some((d) => d.result === "pass");
segments.push({ text: "Received a " });
segments.push({
text: dkimPassed ? "DKIM-signed" : "non-DKIM-signed",
highlight: { color: dkimPassed ? "good" : "danger", bold: true },
link: "#authentication-dkim"
link: "#authentication-dkim",
});
segments.push({ text: " email from " });
segments.push({
text: mailFrom,
highlight: { emphasis: true }
highlight: { emphasis: true },
});
// Server information and hops
@ -47,12 +49,12 @@
segments.push({
text: serverName,
highlight: { monospace: true },
link: "#header-details"
link: "#header-details",
});
segments.push({ text: " after " });
segments.push({
text: `${hopCount-1} hop${hopCount-1 !== 1 ? "s" : ""}`,
link: "#email-path"
text: `${hopCount - 1} hop${hopCount - 1 !== 1 ? "s" : ""}`,
link: "#email-path",
});
}
@ -65,22 +67,25 @@
segments.push({
text: "authenticated",
highlight: { color: "good", bold: true },
link: "#authentication-details"
link: "#authentication-details",
});
segments.push({ text: " to send email on behalf of " });
segments.push({ text: report.header_analysis?.domain_alignment?.from_domain, highlight: {monospace: true} });
segments.push({
text: report.header_analysis?.domain_alignment?.from_domain || "unknown domain",
highlight: { monospace: true },
});
} else if (spfResult && spfResult !== "none") {
segments.push({
text: "not authenticated",
highlight: { color: "danger", bold: true },
link: "#authentication-spf"
link: "#authentication-spf",
});
segments.push({ text: " (failed authentication checks)" });
} else {
segments.push({
text: "not authenticated",
highlight: { color: "warning", bold: true },
link: "#authentication-details"
link: "#authentication-details",
});
segments.push({ text: " (lacks proper authentication)" });
}
@ -92,21 +97,23 @@
segments.push({
text: "failed",
highlight: { color: "danger", bold: true },
link: "#authentication-spf"
link: "#authentication-spf",
});
segments.push({
text: ", the sending server is not authorized to send mail for this domain",
});
segments.push({ text: ", the sending server is not authorized to send mail for this domain" });
} else if (spfResult === "softfail") {
segments.push({
text: "soft-failed",
highlight: { color: "warning", bold: true },
link: "#authentication-spf"
link: "#authentication-spf",
});
segments.push({ text: ", the sending server may not be authorized" });
} else if (spfResult === "temperror" || spfResult === "permerror") {
segments.push({
text: "encountered an error",
highlight: { color: "warning", bold: true },
link: "#authentication-spf"
link: "#authentication-spf",
});
segments.push({ text: ", check your SPF record configuration" });
} else if (spfResult === "none") {
@ -114,9 +121,11 @@
segments.push({
text: "no SPF record",
highlight: { color: "danger", bold: true },
link: "#dns-spf"
link: "#dns-spf",
});
segments.push({
text: ", you should add one to specify which servers can send email on your behalf",
});
segments.push({ text: ", you should add one to specify which servers can send email on your behalf" });
}
}
@ -129,13 +138,13 @@
segments.push({
text: "good",
highlight: { color: "good", bold: true },
link: "#dns-ptr"
link: "#dns-ptr",
});
} else if (iprevResult.result === "fail") {
segments.push({
text: "failed",
highlight: { color: "danger", bold: true },
link: "#dns-ptr"
link: "#dns-ptr",
});
segments.push({ text: " to pass the test" });
} else {
@ -143,7 +152,7 @@
segments.push({
text: iprevResult.result,
highlight: { color: "warning", bold: true },
link: "#dns-ptr"
link: "#dns-ptr",
});
}
}
@ -152,20 +161,20 @@
const blacklists = report.blacklists;
if (blacklists && Object.keys(blacklists).length > 0) {
const allChecks = Object.values(blacklists).flat();
const listedCount = allChecks.filter(check => check.listed).length;
const listedCount = allChecks.filter((check) => check.listed).length;
segments.push({ text: ". Your server is " });
if (listedCount > 0) {
segments.push({
text: `blacklisted on ${listedCount} list${listedCount !== 1 ? "s" : ""}`,
highlight: { color: "danger", bold: true },
link: "#rbl-details"
link: "#rbl-details",
});
} else {
segments.push({
text: "not blacklisted",
highlight: { color: "good", bold: true },
link: "#rbl-details"
link: "#rbl-details",
});
}
}
@ -178,7 +187,7 @@
segments.push({
text: "good",
highlight: { color: "good", bold: true },
link: "#domain-alignment"
link: "#domain-alignment",
});
if (!domainAlignment.aligned) {
segments.push({ text: " using organizational domain" });
@ -187,17 +196,22 @@
segments.push({
text: "misaligned",
highlight: { color: "danger", bold: true },
link: "#domain-alignment"
link: "#domain-alignment",
});
segments.push({ text: ": " });
segments.push({ text: "Return-Path", highlight: { monospace: true } });
segments.push({ text: " is set to an address of " });
segments.push({ text: report.header_analysis?.domain_alignment?.return_path_domain, highlight: { monospace: true } });
segments.push({
text:
report.header_analysis?.domain_alignment?.return_path_domain ||
"unknown domain",
highlight: { monospace: true },
});
segments.push({ text: ", you should " });
segments.push({
text: "update it",
highlight: { bold: true },
link: "#domain-alignment"
link: "#domain-alignment",
});
}
}
@ -210,25 +224,28 @@
segments.push({
text: "don't have",
highlight: { color: "danger", bold: true },
link: "#dns-dmarc"
link: "#dns-dmarc",
});
segments.push({ text: " a DMARC record, " });
segments.push({ text: "consider adding at least a record with the '", highlight: { bold : true } });
segments.push({
text: "consider adding at least a record with the '",
highlight: { bold: true },
});
segments.push({ text: "none", highlight: { monospace: true, bold: true } });
segments.push({ text: "' policy", highlight: { bold : true } });
segments.push({ text: "' policy", highlight: { bold: true } });
} else if (!dmarcRecord.valid) {
segments.push({ text: ". Your DMARC record has " });
segments.push({
text: "issues",
highlight: { color: "danger", bold: true },
link: "#dns-dmarc"
link: "#dns-dmarc",
});
} else if (dmarcRecord.policy === "none") {
segments.push({ text: ". Your DMARC policy is " });
segments.push({
text: "set to 'none'",
highlight: { color: "warning", bold: true },
link: "#dns-dmarc"
link: "#dns-dmarc",
});
segments.push({ text: ", which provides monitoring but no protection" });
} else if (dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject") {
@ -236,7 +253,7 @@
segments.push({
text: dmarcRecord.policy,
highlight: { color: "good", bold: true, monospace: true },
link: "#dns-dmarc"
link: "#dns-dmarc",
});
segments.push({ text: "'" });
if (dmarcRecord.policy === "reject") {
@ -247,17 +264,17 @@
segments.push({ text: "'" });
}
}
} else if (dmarcResult && dmarcResult.result === "fail") {
} else if (dmarcResult === "fail") {
segments.push({ text: ". DMARC check " });
segments.push({
text: "failed",
highlight: { color: "danger", bold: true },
link: "#authentication-dmarc"
link: "#authentication-dmarc",
});
}
// BIMI
if (dmarcRecord.valid && dmarcRecord.policy != "none") {
if (dmarcRecord && dmarcRecord.valid && dmarcRecord.policy != "none") {
const bimiResult = report.authentication?.bimi;
const bimiRecord = report.dns_results?.bimi_record;
if (bimiRecord?.valid) {
@ -268,7 +285,7 @@
link: "#dns-bimi"
});
segments.push({ text: " for brand indicator display" });
} else if (bimiResult && bimiResult.details.indexOf("(No BIMI records found)") >= 0) {
} else if (bimiResult && bimiResult.details && bimiResult.details.indexOf("(No BIMI records found)") >= 0) {
segments.push({ text: ". Your domain has no " });
segments.push({
text: "BIMI record",
@ -293,19 +310,21 @@
segments.push({ text: ". " });
segments.push({
text: "ARC chain validation",
link: "#authentication-arc"
link: "#authentication-arc",
});
segments.push({ text: " " });
if (arcResult.chain_valid) {
segments.push({
text: "passed",
highlight: { color: "good", bold: true }
highlight: { color: "good", bold: true },
});
segments.push({
text: ` with ${arcResult.chain_length} set${arcResult.chain_length !== 1 ? "s" : ""}, indicating proper email forwarding`,
});
segments.push({ text: ` with ${arcResult.chain_length} set${arcResult.chain_length !== 1 ? "s" : ""}, indicating proper email forwarding` });
} else {
segments.push({
text: "failed",
highlight: { color: "danger", bold: true }
highlight: { color: "danger", bold: true },
});
segments.push({ text: ", which may indicate issues with email forwarding" });
}
@ -316,20 +335,25 @@
const listUnsubscribe = headers?.["list-unsubscribe"];
const listUnsubscribePost = headers?.["list-unsubscribe-post"];
const hasNewsletterHeaders = (listUnsubscribe?.importance === "newsletter" && listUnsubscribe?.present) ||
(listUnsubscribePost?.importance === "newsletter" && listUnsubscribePost?.present);
const hasNewsletterHeaders =
(listUnsubscribe?.importance === "newsletter" && listUnsubscribe?.present) ||
(listUnsubscribePost?.importance === "newsletter" && listUnsubscribePost?.present);
if (!hasNewsletterHeaders && (listUnsubscribe?.importance === "newsletter" || listUnsubscribePost?.importance === "newsletter")) {
if (
!hasNewsletterHeaders &&
(listUnsubscribe?.importance === "newsletter" ||
listUnsubscribePost?.importance === "newsletter")
) {
segments.push({ text: ". This email is " });
segments.push({
text: "missing unsubscribe headers",
highlight: { color: "warning", bold: true },
link: "#header-details"
link: "#header-details",
});
segments.push({ text: " and is " });
segments.push({
text: "not suitable for marketing campaigns",
highlight: { bold: true }
highlight: { bold: true },
});
}
@ -344,7 +368,7 @@
segments.push({
text: "flagged as spam",
highlight: { color: "danger", bold: true },
link: "#spam-details"
link: "#spam-details",
});
segments.push({ text: " and needs review" });
} else if (contentScore < 50) {
@ -352,49 +376,55 @@
segments.push({
text: "needs improvement",
highlight: { color: "warning", bold: true },
link: "#content-details"
link: "#content-details",
});
} else if (contentScore >= 100 && spamScore >= 100) {
segments.push({ text: "Content " });
segments.push({
text: "looks great",
highlight: { color: "good", bold: true },
link: "#content-details"
link: "#content-details",
});
} else if (spamScore < 50) {
segments.push({ text: "Your " });
segments.push({
text: "spam score",
highlight: { color: "danger", bold: true },
link: "#spam-details"
link: "#spam-details",
});
segments.push({ text: " is low" });
if (report.spamassassin.tests.includes("EMPTY_MESSAGE")) {
segments.push({ text: " (you sent an empty message, which can cause this issue, retry with some real content)", highlight: { bold: true } });
if (report.spamassassin?.tests?.includes("EMPTY_MESSAGE")) {
segments.push({
text: " (you sent an empty message, which can cause this issue, retry with some real content)",
highlight: { bold: true },
});
}
} else if (spamScore < 90) {
segments.push({ text: "Pay attention to your " });
segments.push({
text: "spam score",
highlight: { color: "warning", bold: true },
link: "#spam-details"
link: "#spam-details",
});
if (report.spamassassin.tests.includes("EMPTY_MESSAGE")) {
segments.push({ text: " (you sent an empty message, which can cause this issue, retry with some real content)", highlight: { bold: true } });
if (report.spamassassin?.tests?.includes("EMPTY_MESSAGE")) {
segments.push({
text: " (you sent an empty message, which can cause this issue, retry with some real content)",
highlight: { bold: true },
});
}
} else if (contentScore >= 80) {
segments.push({ text: "Content " });
segments.push({
text: "looks good",
highlight: { color: "good", bold: true },
link: "#content-details"
link: "#content-details",
});
} else {
segments.push({ text: "Content " });
segments.push({
text: "should be reviewed",
highlight: { color: "warning", bold: true },
link: "#content-details"
link: "#content-details",
});
}
@ -403,7 +433,7 @@
return segments;
}
function getColorClass(color: "good" | "warning" | "danger"): string {
function getColorClass(color?: "good" | "warning" | "danger"): string {
switch (color) {
case "good":
return "text-success";
@ -411,28 +441,14 @@
return "text-warning";
case "danger":
return "text-danger";
default:
return "";
}
}
const summarySegments = $derived(buildSummary());
</script>
<style>
.summary-link {
text-decoration: none;
transition: opacity 0.2s ease;
}
.summary-link:hover {
opacity: 0.8;
text-decoration: underline;
}
.highlighted {
font-weight: 600;
}
</style>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body p-4">
<h5 class="card-title mb-3">
@ -460,3 +476,19 @@
</p>
</div>
</div>
<style>
.summary-link {
text-decoration: none;
transition: opacity 0.2s ease;
}
.summary-link:hover {
opacity: 0.8;
text-decoration: underline;
}
.highlighted {
font-weight: 600;
}
</style>

View file

@ -7,8 +7,8 @@ export class NotAuthorizedError extends Error {
}
}
async function customFetch(url: string, init: RequestInit): Promise<Response> {
const response = await fetch(url, init);
async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const response = await fetch(input, init);
if (response.status === 400) {
const json = await response.json();

View file

@ -38,7 +38,12 @@
</h3>
<ul class="footer-links">
<li><a href="/#features">Features</a></li>
<li><a href="#">Download</a></li>
<li>
<a
href="https://github.com/happyDomain/happydeliver/releases"
target="_blank">Download</a
>
</li>
<li>
<a href="https://github.com/happyDomain/happydeliver/" target="_blank">
GitHub
@ -73,16 +78,32 @@
class="d-flex flex-wrap justify-content-between footer-links"
style="gap: .5em; font-size: 2em"
>
<a href="https://framagit.org/happyDomain/happydeliver" target="_blank">
<a
href="https://framagit.org/happyDomain/happydeliver"
target="_blank"
aria-label="Visit our GitLab repository"
>
<i class="bi bi-gitlab"></i>
</a>
<a href="https://github.com/happyDomain/happydeliver" target="_blank">
<a
href="https://github.com/happyDomain/happydeliver"
target="_blank"
aria-label="Visit our GitHub repository"
>
<i class="bi bi-github"></i>
</a>
<a href="https://feedback.happydomain.org/" target="_blank">
<a
href="https://feedback.happydomain.org/"
target="_blank"
aria-label="Share your feedback"
>
<i class="bi bi-lightbulb-fill"></i>
</a>
<a href="https://floss.social/@happyDomain" target="_blank">
<a
href="https://floss.social/@happyDomain"
target="_blank"
aria-label="Follow us on Mastodon"
>
<i class="bi bi-mastodon"></i>
</a>
</div>

View file

@ -10,10 +10,11 @@ export const load: Load = async ({}) => {
try {
response = await apiCreateTest();
} catch (err) {
error(err.response.status, err.message);
const errorObj = err as { response?: { status?: number }; message?: string };
error(errorObj.response?.status || 500, errorObj.message || "Unknown error");
}
if (response.response.ok) {
if (response.response.ok && response.data) {
redirect(302, `/test/${response.data.id}`);
} else {
error(response.response.status, response.error);

View file

@ -28,6 +28,8 @@
let fetching = $state(false);
async function fetchTest() {
if (!testId) return;
if (nbfetch > 0) {
nextfetch = Math.max(nextfetch, Math.floor(3 + nbfetch * 0.5));
}
@ -89,8 +91,8 @@
}
}
let lastTestId = null;
function testChange(newTestId) {
let lastTestId: string | null = null;
function testChange(newTestId: string) {
if (lastTestId != newTestId) {
lastTestId = newTestId;
test = null;
@ -100,7 +102,10 @@
}
$effect(() => {
testChange(page.params.test);
const newTestId = page.params.test;
if (newTestId) {
testChange(newTestId);
}
})
onDestroy(() => {
@ -128,9 +133,9 @@
function handleExportJSON() {
const dataStr = JSON.stringify(report, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const dataBlob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = `report-${testId}.json`;
link.click();
@ -140,7 +145,7 @@
</script>
<svelte:head>
<title>{report ? `Test of ${report.dns_results.from_domain} ${report.test_id.slice(0, 7)}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver</title>
<title>{report ? `Test${report.dns_results ? ` of ${report.dns_results.from_domain}` : ''} ${report.test_id?.slice(0, 7) || ''}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver</title>
</svelte:head>
<div class="container py-5">
@ -283,11 +288,11 @@
<div class="row mb-4" id="header">
<div class="col-12">
<HeaderAnalysisCard
dmarcRecord={report.dns_results.dmarc_record}
dmarcRecord={report.dns_results?.dmarc_record}
headerAnalysis={report.header_analysis}
headerGrade={report.summary?.header_grade}
headerScore={report.summary?.header_score}
xAlignedFrom={report.authentication.x_aligned_from}
xAlignedFrom={report.authentication?.x_aligned_from}
/>
</div>
</div>
@ -348,23 +353,6 @@
}
}
.category-section {
margin-bottom: 2rem;
}
.category-title {
font-size: 1.25rem;
font-weight: 600;
color: #495057;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e9ecef;
}
.category-score {
font-size: 1rem;
font-weight: 700;
}
.menu-container {
position: relative;
}