From 474f25007b6b5665c89bf0a3ecdb667e49bd8b31 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 24 Oct 2025 17:20:35 +0700 Subject: [PATCH 01/13] Fix typescript/svelte checks --- .../lib/components/AuthenticationCard.svelte | 34 ++-- .../lib/components/BimiRecordDisplay.svelte | 4 +- web/src/lib/components/BlacklistCard.svelte | 4 +- .../lib/components/DkimRecordsDisplay.svelte | 4 +- .../lib/components/DmarcRecordDisplay.svelte | 4 +- web/src/lib/components/DnsRecordsCard.svelte | 4 +- web/src/lib/components/GradeDisplay.svelte | 2 +- .../lib/components/HeaderAnalysisCard.svelte | 6 +- .../lib/components/MxRecordsDisplay.svelte | 6 +- web/src/lib/components/PendingState.svelte | 2 + .../lib/components/SpamAssassinCard.svelte | 4 +- .../lib/components/SpfRecordsDisplay.svelte | 20 +- web/src/lib/components/SummaryCard.svelte | 182 ++++++++++-------- web/src/lib/hey-api.ts | 4 +- web/src/routes/+layout.svelte | 31 ++- web/src/routes/test/+page.ts | 5 +- web/src/routes/test/[test]/+page.svelte | 38 ++-- 17 files changed, 199 insertions(+), 155 deletions(-) diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index cf1b80f..b76b48a 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -1,13 +1,13 @@ @@ -58,13 +58,13 @@ {#if spf.all_qualifier}
All Mechanism Policy: - {#if spf.all_qualifier === '-'} + {#if spf.all_qualifier === "-"} Strict (-all) - {:else if spf.all_qualifier === '~'} + {:else if spf.all_qualifier === "~"} Softfail (~all) - {:else if spf.all_qualifier === '+'} + {:else if spf.all_qualifier === "+"} Pass (+all) - {:else if spf.all_qualifier === '?'} + {:else if spf.all_qualifier === "?"} Neutral (?all) {/if} {#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))} diff --git a/web/src/lib/components/SummaryCard.svelte b/web/src/lib/components/SummaryCard.svelte index 971c1ac..9eb6272 100644 --- a/web/src/lib/components/SummaryCard.svelte +++ b/web/src/lib/components/SummaryCard.svelte @@ -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()); - -
@@ -460,3 +476,19 @@

+ + diff --git a/web/src/lib/hey-api.ts b/web/src/lib/hey-api.ts index e75e70a..6983e5d 100644 --- a/web/src/lib/hey-api.ts +++ b/web/src/lib/hey-api.ts @@ -7,8 +7,8 @@ export class NotAuthorizedError extends Error { } } -async function customFetch(url: string, init: RequestInit): Promise { - const response = await fetch(url, init); +async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const response = await fetch(input, init); if (response.status === 400) { const json = await response.json(); diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index f0031bb..35cf00e 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -38,7 +38,12 @@
diff --git a/web/src/routes/test/+page.ts b/web/src/routes/test/+page.ts index d2f88f2..8f8fd5b 100644 --- a/web/src/routes/test/+page.ts +++ b/web/src/routes/test/+page.ts @@ -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); diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 8ac67eb..7ef2b63 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -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 @@ - {report ? `Test of ${report.dns_results.from_domain} ${report.test_id.slice(0, 7)}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver + {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
@@ -283,11 +288,11 @@ @@ -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; } -- 2.52.0 From 82f21abcfff135fe77c920b7c42f413252e420a6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 24 Oct 2025 17:35:58 +0700 Subject: [PATCH 02/13] Update features on home page --- web/routes.go | 4 +++ web/src/lib/config.ts | 48 ++++++++++++++++++++++++++++ web/src/routes/+page.svelte | 62 +++++++++++++++++++++++++++++-------- 3 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 web/src/lib/config.ts diff --git a/web/routes.go b/web/routes.go index f67b453..184da64 100644 --- a/web/routes.go +++ b/web/routes.go @@ -54,6 +54,10 @@ func init() { func DeclareRoutes(cfg *config.Config, router *gin.Engine) { appConfig := map[string]interface{}{} + if cfg.ReportRetention > 0 { + appConfig["report_retention"] = cfg.ReportRetention + } + if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil { log.Println("Unable to generate JSON config to inject in web application") } else { diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts new file mode 100644 index 0000000..65eb1bb --- /dev/null +++ b/web/src/lib/config.ts @@ -0,0 +1,48 @@ +// This file is part of the happyDeliver (R) project. +// Copyright (c) 2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { writable } from "svelte/store"; + +interface AppConfig { + report_retention?: number; +} + +const defaultConfig: AppConfig = { + report_retention: 0, +}; + +function getConfigFromScriptTag(): AppConfig | null { + if (typeof document !== "undefined") { + const configScript = document.getElementById("app-config"); + if (configScript) { + try { + return JSON.parse(configScript.textContent || ""); + } catch (e) { + console.error("Failed to parse app config:", e); + } + } + } + return null; +} + +const initialConfig = getConfigFromScriptTag() || defaultConfig; + +export const appConfig = writable(initialConfig); diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index ecfbbdd..8783582 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,6 +1,7 @@ + +{#if $appConfig.surveyUrl} +
+ {#if step === 0} + {#if question}{@render question()}{:else} +

Help us to design a better tool, rate this report!

+ {/if} +
+ {#each [...Array(5).keys()] as i} + + {/each} +
+ {:else if step === 1} +

+ {#if responses.stars == 5}Thank you! Would you like to tell us more? + {:else if responses.stars == 4}What are we missing to earn 5 stars? + {:else}How could we improve? + {/if} +

+ + + + {:else if step === 2} +

+ Thank you so much for taking the time to share your feedback! +

+ {/if} +
+{/if} diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index 8b83ae5..e600c11 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -13,3 +13,4 @@ export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte"; export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte"; export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte"; export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte"; +export { default as TinySurvey } from "./TinySurvey.svelte"; diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts index 65eb1bb..c4c0bd4 100644 --- a/web/src/lib/config.ts +++ b/web/src/lib/config.ts @@ -23,10 +23,12 @@ import { writable } from "svelte/store"; interface AppConfig { report_retention?: number; + surveyUrl?: string; } const defaultConfig: AppConfig = { report_retention: 0, + surveyUrl: "", }; function getConfigFromScriptTag(): AppConfig | null { diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 7ef2b63..7f50923 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -12,7 +12,8 @@ DnsRecordsCard, BlacklistCard, ContentAnalysisCard, - HeaderAnalysisCard + HeaderAnalysisCard, + TinySurvey, } from "$lib/components"; let testId = $derived(page.params.test); @@ -236,7 +237,11 @@
- + +
+ +
+
-- 2.52.0 From 8cb13b912fe60cec0ef53bf9755b5d8d6eb370a1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 24 Oct 2025 18:27:51 +0700 Subject: [PATCH 05/13] Handle errors on test page --- web/src/lib/components/ErrorDisplay.svelte | 158 +++++++++++++++++++++ web/src/lib/components/index.ts | 1 + web/src/routes/+error.svelte | 126 +--------------- web/src/routes/test/[test]/+page.svelte | 57 ++++++-- 4 files changed, 209 insertions(+), 133 deletions(-) create mode 100644 web/src/lib/components/ErrorDisplay.svelte diff --git a/web/src/lib/components/ErrorDisplay.svelte b/web/src/lib/components/ErrorDisplay.svelte new file mode 100644 index 0000000..96cfae2 --- /dev/null +++ b/web/src/lib/components/ErrorDisplay.svelte @@ -0,0 +1,158 @@ + + +
+
+ +
+ +
+ + +

{status}

+ + +

{getErrorTitle(status)}

+ + +

{getErrorDescription(status)}

+ + + {#if message && message !== defaultDescription} + + {/if} + + + {#if showActions} +
+ + + Go Home + + +
+ {/if} + + + {#if status === 404 && showActions} +
+

Looking for something specific?

+ +
+ {/if} +
+
+ + diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index e600c11..dadab9e 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -14,3 +14,4 @@ export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte"; export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte"; export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte"; export { default as TinySurvey } from "./TinySurvey.svelte"; +export { default as ErrorDisplay } from "./ErrorDisplay.svelte"; diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte index 5d0514c..a429ea5 100644 --- a/web/src/routes/+error.svelte +++ b/web/src/routes/+error.svelte @@ -1,5 +1,6 @@ @@ -55,96 +28,5 @@
-
-
- -
- -
- - -

{status}

- - -

{getErrorTitle(status)}

- - -

{getErrorDescription(status)}

- - - {#if message !== getErrorDescription(status)} - - {/if} - - -
- - - Go Home - - -
- - - {#if status === 404} -
-

Looking for something specific?

- -
- {/if} -
-
+
- - diff --git a/web/src/routes/test/[test]/+page.svelte b/web/src/routes/test/[test]/+page.svelte index 7f50923..c8b5cc0 100644 --- a/web/src/routes/test/[test]/+page.svelte +++ b/web/src/routes/test/[test]/+page.svelte @@ -14,6 +14,7 @@ ContentAnalysisCard, HeaderAnalysisCard, TinySurvey, + ErrorDisplay, } from "$lib/components"; let testId = $derived(page.params.test); @@ -21,6 +22,7 @@ let report = $state(null); let loading = $state(true); let error = $state(null); + let errorStatus = $state(500); let reanalyzing = $state(false); let pollInterval: ReturnType | null = null; let nextfetch = $state(23); @@ -28,6 +30,36 @@ let menuOpen = $state(false); let fetching = $state(false); + // Helper function to handle API errors + function handleApiError(apiError: unknown, defaultMessage: string) { + if (apiError && typeof apiError === "object") { + if ("message" in apiError) { + error = String(apiError.message); + } else { + error = defaultMessage; + } + + // Determine status code based on error type + if ("error" in apiError) { + if (apiError.error === "rate_limit_exceeded") { + errorStatus = 429; + } else if (apiError.error === "not_found") { + errorStatus = 404; + } else { + errorStatus = 500; + } + } else { + errorStatus = 500; + } + } else if (apiError instanceof Error) { + error = apiError.message; + errorStatus = 500; + } else { + error = defaultMessage; + errorStatus = 500; + } + } + async function fetchTest() { if (!testId) return; @@ -36,6 +68,9 @@ } nbfetch += 1; + // Clear any previous errors + error = null; + // Set fetching state and ensure minimum 500ms display time fetching = true; const startTime = Date.now(); @@ -52,10 +87,15 @@ } stopPolling(); } + } else if (testResponse.error) { + handleApiError(testResponse.error, "Failed to fetch test"); + loading = false; + stopPolling(); + return; } loading = false; } catch (err) { - error = err instanceof Error ? err.message : "Failed to fetch test"; + handleApiError(err, "Failed to fetch test"); loading = false; stopPolling(); } finally { @@ -107,7 +147,7 @@ if (newTestId) { testChange(newTestId); } - }) + }); onDestroy(() => { stopPolling(); @@ -124,9 +164,11 @@ const response = await reanalyzeReport({ path: { id: testId } }); if (response.data) { report = response.data; + } else if (response.error) { + handleApiError(response.error, "Failed to reanalyze report"); } } catch (err) { - error = err instanceof Error ? err.message : "Failed to reanalyze report"; + handleApiError(err, "Failed to reanalyze report"); } finally { reanalyzing = false; } @@ -162,14 +204,7 @@

Loading test...

{:else if error} -
-
- -
-
+ {:else if test && test.status !== "analyzed"} Date: Fri, 24 Oct 2025 18:43:55 +0700 Subject: [PATCH 06/13] Add a background report sample in hero on home page Why use a report from icloud.com to illustrate this project? Among all the common email providers I tested, it achieved the best results. --- web/src/routes/+page.svelte | 41 ++++++++++++++++++++++++++++++++++-- web/static/img/report.webp | Bin 0 -> 86668 bytes 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 web/static/img/report.webp diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 8783582..f26f8e2 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -151,7 +151,7 @@ and more. Open-source, self-hosted, and privacy-focused.