Handle errors on test page
This commit is contained in:
parent
a741570a36
commit
8cb13b912f
4 changed files with 209 additions and 133 deletions
158
web/src/lib/components/ErrorDisplay.svelte
Normal file
158
web/src/lib/components/ErrorDisplay.svelte
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
status: number;
|
||||||
|
message?: string;
|
||||||
|
showActions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { status, message, showActions = true }: Props = $props();
|
||||||
|
|
||||||
|
function getErrorTitle(status: number): string {
|
||||||
|
switch (status) {
|
||||||
|
case 404:
|
||||||
|
return "Page Not Found";
|
||||||
|
case 403:
|
||||||
|
return "Access Denied";
|
||||||
|
case 429:
|
||||||
|
return "Too Many Requests";
|
||||||
|
case 500:
|
||||||
|
return "Server Error";
|
||||||
|
case 503:
|
||||||
|
return "Service Unavailable";
|
||||||
|
default:
|
||||||
|
return "Something Went Wrong";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorDescription(status: number): string {
|
||||||
|
switch (status) {
|
||||||
|
case 404:
|
||||||
|
return "The page you're looking for doesn't exist or has been moved.";
|
||||||
|
case 403:
|
||||||
|
return "You don't have permission to access this resource.";
|
||||||
|
case 429:
|
||||||
|
return "You've made too many requests. Please wait a moment and try again.";
|
||||||
|
case 500:
|
||||||
|
return "Our server encountered an error while processing your request.";
|
||||||
|
case 503:
|
||||||
|
return "The service is temporarily unavailable. Please try again later.";
|
||||||
|
default:
|
||||||
|
return "An unexpected error occurred. Please try again.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorIcon(status: number): string {
|
||||||
|
switch (status) {
|
||||||
|
case 404:
|
||||||
|
return "bi-search";
|
||||||
|
case 403:
|
||||||
|
return "bi-shield-lock";
|
||||||
|
case 429:
|
||||||
|
return "bi-hourglass-split";
|
||||||
|
case 500:
|
||||||
|
return "bi-exclamation-triangle";
|
||||||
|
case 503:
|
||||||
|
return "bi-clock-history";
|
||||||
|
default:
|
||||||
|
return "bi-exclamation-circle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultDescription = $derived(getErrorDescription(status));
|
||||||
|
let displayMessage = $derived(message || defaultDescription);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6 text-center fade-in">
|
||||||
|
<!-- Error Icon -->
|
||||||
|
<div class="error-icon-wrapper mb-4">
|
||||||
|
<i class="bi {getErrorIcon(status)} text-danger"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Status -->
|
||||||
|
<h1 class="display-1 fw-bold text-primary mb-3">{status}</h1>
|
||||||
|
|
||||||
|
<!-- Error Title -->
|
||||||
|
<h2 class="fw-bold mb-3">{getErrorTitle(status)}</h2>
|
||||||
|
|
||||||
|
<!-- Error Description -->
|
||||||
|
<p class="text-muted mb-4">{getErrorDescription(status)}</p>
|
||||||
|
|
||||||
|
<!-- Error Message (if available and different from default) -->
|
||||||
|
{#if message && message !== defaultDescription}
|
||||||
|
<div class="alert alert-light border mb-4" role="alert">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
{#if showActions}
|
||||||
|
<div class="d-flex flex-column flex-sm-row gap-3 justify-content-center">
|
||||||
|
<a href="/" class="btn btn-primary btn-lg px-4">
|
||||||
|
<i class="bi bi-house-door me-2"></i>
|
||||||
|
Go Home
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary btn-lg px-4"
|
||||||
|
onclick={() => window.history.back()}
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Additional Help -->
|
||||||
|
{#if status === 404 && showActions}
|
||||||
|
<div class="mt-5">
|
||||||
|
<p class="text-muted small mb-2">Looking for something specific?</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2 justify-content-center">
|
||||||
|
<a href="/" class="badge bg-light text-dark text-decoration-none">Home</a>
|
||||||
|
<a href="/#features" class="badge bg-light text-dark text-decoration-none">
|
||||||
|
Features
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/happyDomain/happydeliver"
|
||||||
|
class="badge bg-light text-dark text-decoration-none"
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error-icon-wrapper {
|
||||||
|
font-size: 6rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -14,3 +14,4 @@ export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
|
||||||
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
|
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
|
||||||
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
|
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
|
||||||
export { default as TinySurvey } from "./TinySurvey.svelte";
|
export { default as TinySurvey } from "./TinySurvey.svelte";
|
||||||
|
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
import { ErrorDisplay } from "$lib/components";
|
||||||
|
|
||||||
let status = $derived($page.status);
|
let status = $derived($page.status);
|
||||||
let message = $derived($page.error?.message || "An unexpected error occurred");
|
let message = $derived($page.error?.message || "An unexpected error occurred");
|
||||||
|
|
@ -10,6 +11,8 @@
|
||||||
return "Page Not Found";
|
return "Page Not Found";
|
||||||
case 403:
|
case 403:
|
||||||
return "Access Denied";
|
return "Access Denied";
|
||||||
|
case 429:
|
||||||
|
return "Too Many Requests";
|
||||||
case 500:
|
case 500:
|
||||||
return "Server Error";
|
return "Server Error";
|
||||||
case 503:
|
case 503:
|
||||||
|
|
@ -18,36 +21,6 @@
|
||||||
return "Something Went Wrong";
|
return "Something Went Wrong";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getErrorDescription(status: number): string {
|
|
||||||
switch (status) {
|
|
||||||
case 404:
|
|
||||||
return "The page you're looking for doesn't exist or has been moved.";
|
|
||||||
case 403:
|
|
||||||
return "You don't have permission to access this resource.";
|
|
||||||
case 500:
|
|
||||||
return "Our server encountered an error while processing your request.";
|
|
||||||
case 503:
|
|
||||||
return "The service is temporarily unavailable. Please try again later.";
|
|
||||||
default:
|
|
||||||
return "An unexpected error occurred. Please try again.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorIcon(status: number): string {
|
|
||||||
switch (status) {
|
|
||||||
case 404:
|
|
||||||
return "bi-search";
|
|
||||||
case 403:
|
|
||||||
return "bi-shield-lock";
|
|
||||||
case 500:
|
|
||||||
return "bi-exclamation-triangle";
|
|
||||||
case 503:
|
|
||||||
return "bi-clock-history";
|
|
||||||
default:
|
|
||||||
return "bi-exclamation-circle";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -55,96 +28,5 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<div class="row justify-content-center">
|
<ErrorDisplay {status} {message} />
|
||||||
<div class="col-lg-6 text-center fade-in">
|
|
||||||
<!-- Error Icon -->
|
|
||||||
<div class="error-icon-wrapper mb-4">
|
|
||||||
<i class="bi {getErrorIcon(status)} text-danger"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error Status -->
|
|
||||||
<h1 class="display-1 fw-bold text-primary mb-3">{status}</h1>
|
|
||||||
|
|
||||||
<!-- Error Title -->
|
|
||||||
<h2 class="fw-bold mb-3">{getErrorTitle(status)}</h2>
|
|
||||||
|
|
||||||
<!-- Error Description -->
|
|
||||||
<p class="text-muted mb-4">{getErrorDescription(status)}</p>
|
|
||||||
|
|
||||||
<!-- Error Message (if available) -->
|
|
||||||
{#if message !== getErrorDescription(status)}
|
|
||||||
<div class="alert alert-light border mb-4" role="alert">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="d-flex flex-column flex-sm-row gap-3 justify-content-center">
|
|
||||||
<a href="/" class="btn btn-primary btn-lg px-4">
|
|
||||||
<i class="bi bi-house-door me-2"></i>
|
|
||||||
Go Home
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-primary btn-lg px-4"
|
|
||||||
onclick={() => window.history.back()}
|
|
||||||
>
|
|
||||||
<i class="bi bi-arrow-left me-2"></i>
|
|
||||||
Go Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Help -->
|
|
||||||
{#if status === 404}
|
|
||||||
<div class="mt-5">
|
|
||||||
<p class="text-muted small mb-2">Looking for something specific?</p>
|
|
||||||
<div class="d-flex flex-wrap gap-2 justify-content-center">
|
|
||||||
<a href="/" class="badge bg-light text-dark text-decoration-none">Home</a>
|
|
||||||
<a href="/#features" class="badge bg-light text-dark text-decoration-none"
|
|
||||||
>Features</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://github.com/happyDomain/happydeliver"
|
|
||||||
class="badge bg-light text-dark text-decoration-none"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.error-icon-wrapper {
|
|
||||||
font-size: 6rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in {
|
|
||||||
animation: fadeIn 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-weight: normal;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
ContentAnalysisCard,
|
ContentAnalysisCard,
|
||||||
HeaderAnalysisCard,
|
HeaderAnalysisCard,
|
||||||
TinySurvey,
|
TinySurvey,
|
||||||
|
ErrorDisplay,
|
||||||
} from "$lib/components";
|
} from "$lib/components";
|
||||||
|
|
||||||
let testId = $derived(page.params.test);
|
let testId = $derived(page.params.test);
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
let report = $state<Report | null>(null);
|
let report = $state<Report | null>(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
let errorStatus = $state<number>(500);
|
||||||
let reanalyzing = $state(false);
|
let reanalyzing = $state(false);
|
||||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let nextfetch = $state(23);
|
let nextfetch = $state(23);
|
||||||
|
|
@ -28,6 +30,36 @@
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
let fetching = $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() {
|
async function fetchTest() {
|
||||||
if (!testId) return;
|
if (!testId) return;
|
||||||
|
|
||||||
|
|
@ -36,6 +68,9 @@
|
||||||
}
|
}
|
||||||
nbfetch += 1;
|
nbfetch += 1;
|
||||||
|
|
||||||
|
// Clear any previous errors
|
||||||
|
error = null;
|
||||||
|
|
||||||
// Set fetching state and ensure minimum 500ms display time
|
// Set fetching state and ensure minimum 500ms display time
|
||||||
fetching = true;
|
fetching = true;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
@ -52,10 +87,15 @@
|
||||||
}
|
}
|
||||||
stopPolling();
|
stopPolling();
|
||||||
}
|
}
|
||||||
|
} else if (testResponse.error) {
|
||||||
|
handleApiError(testResponse.error, "Failed to fetch test");
|
||||||
|
loading = false;
|
||||||
|
stopPolling();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : "Failed to fetch test";
|
handleApiError(err, "Failed to fetch test");
|
||||||
loading = false;
|
loading = false;
|
||||||
stopPolling();
|
stopPolling();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -107,7 +147,7 @@
|
||||||
if (newTestId) {
|
if (newTestId) {
|
||||||
testChange(newTestId);
|
testChange(newTestId);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
|
@ -124,9 +164,11 @@
|
||||||
const response = await reanalyzeReport({ path: { id: testId } });
|
const response = await reanalyzeReport({ path: { id: testId } });
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
report = response.data;
|
report = response.data;
|
||||||
|
} else if (response.error) {
|
||||||
|
handleApiError(response.error, "Failed to reanalyze report");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : "Failed to reanalyze report";
|
handleApiError(err, "Failed to reanalyze report");
|
||||||
} finally {
|
} finally {
|
||||||
reanalyzing = false;
|
reanalyzing = false;
|
||||||
}
|
}
|
||||||
|
|
@ -162,14 +204,7 @@
|
||||||
<p class="mt-3 text-muted">Loading test...</p>
|
<p class="mt-3 text-muted">Loading test...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<div class="row justify-content-center">
|
<ErrorDisplay status={errorStatus} message={error} showActions={false} />
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if test && test.status !== "analyzed"}
|
{:else if test && test.status !== "analyzed"}
|
||||||
<!-- Pending State -->
|
<!-- Pending State -->
|
||||||
<PendingState
|
<PendingState
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue