Add SHD themed error handling
This commit is contained in:
@@ -13,6 +13,24 @@ This document defines the visual language for SHD.build. New pages should feel l
|
||||
underlines. Communicate hover and focus through SHD accent color, borders,
|
||||
brightness, and the required focus-visible outline.
|
||||
|
||||
### Routed error console
|
||||
|
||||
Expected HTTP errors and unexpected server faults use one shared SHD exception
|
||||
console inside the normal application shell. The console always displays the
|
||||
numeric status, an operational title, a safe explanation, request path, node,
|
||||
and context-appropriate recovery controls. Common statuses receive distinct
|
||||
language: authentication for 401, clearance for 403, lost location for 404,
|
||||
conflict for 409, saturation for 429, and service/fault language for 5xx.
|
||||
|
||||
Orange identifies recoverable request errors and actions; red identifies server
|
||||
faults. Never display stack traces, database messages, credentials, request
|
||||
queries, or raw unexpected exceptions to users. Unexpected server faults receive
|
||||
a short reference ID from the global error handler for support correlation while
|
||||
the complete error remains server-side. If the root application shell itself
|
||||
cannot render, `src/error.html` provides a dependency-free SHD fallback surface.
|
||||
Error controls retain square geometry, focus-visible treatment, pressed motion,
|
||||
plain-language labels, and stacked mobile recovery actions.
|
||||
|
||||
## Tokens
|
||||
|
||||
The canonical tokens live in `src/app.css` under `:root`.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+4
-1
@@ -2,7 +2,10 @@
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Error {
|
||||
message: string;
|
||||
errorId?: string;
|
||||
}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>%sveltekit.status% · SHD.build system fault</title>
|
||||
<style>
|
||||
:root{color-scheme:dark;font-family:Arial,Helvetica,sans-serif;background:#0a0c0d;color:#ecebe5}*{box-sizing:border-box}body{min-height:100vh;margin:0;display:grid;place-items:center;padding:24px;background:linear-gradient(135deg,#101314 0,#0a0c0d 55%)}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.08;background-image:linear-gradient(#ffffff14 1px,transparent 1px),linear-gradient(90deg,#ffffff14 1px,transparent 1px);background-size:48px 48px}main{position:relative;width:min(760px,100%);border:1px solid #3b4142;border-top:2px solid #df8069;background:#111415}header{height:64px;display:flex;align-items:center;justify-content:space-between;padding:0 20px;border-bottom:1px solid #303536}header div{display:grid;gap:4px}header span{color:#858b8d;font-size:8px;font-weight:900;letter-spacing:1.5px}header strong{color:#df8069;font-size:11px;letter-spacing:1px}header b{font-size:32px;color:#df8069}section{padding:clamp(28px,6vw,58px)}p{color:#858b8d;font:15px/1.6 Georgia,serif}small{display:block;color:#f6a821;font-size:8px;font-weight:900;letter-spacing:1.8px}h1{max-width:620px;margin:14px 0;font-size:clamp(34px,7vw,62px);line-height:.92;letter-spacing:-2px}a{display:inline-block;margin-top:20px;padding:14px 18px;background:#f6a821;color:#111;font-size:9px;font-weight:900;letter-spacing:1.2px;text-decoration:none}footer{display:flex;align-items:center;gap:8px;padding:12px 20px;border-top:1px solid #303536;color:#697174;font-size:7px;letter-spacing:1px}footer i{width:6px;height:6px;background:#df8069;box-shadow:0 0 7px #df8069}
|
||||
</style>
|
||||
</head>
|
||||
<body><main><header><div><span>ISAC FALLBACK CHANNEL</span><strong>CORE SHELL INTERRUPTED</strong></div><b>%sveltekit.status%</b></header><section><small>SHD NETWORK RESPONSE</small><h1>SYSTEM INTERFACE UNAVAILABLE</h1><p>%sveltekit.error.message%</p><a href="/">RETURN TO SHD.BUILD →</a></section><footer><i></i> FAULT ISOLATED · CORE SERVICES PROTECTED</footer></main></body>
|
||||
</html>
|
||||
+11
-1
@@ -1,4 +1,5 @@
|
||||
import { json, type Handle } from '@sveltejs/kit';
|
||||
import { json, type Handle, type HandleServerError } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { DATA_MODULES, queueDataProposal, shouldModerateDataMutation, type DataModule } from '$lib/server/data-moderation';
|
||||
|
||||
@@ -30,3 +31,12 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
}
|
||||
return resolve(event);
|
||||
};
|
||||
|
||||
export const handleError: HandleServerError = ({ error, event, status, message }) => {
|
||||
const errorId = randomUUID().slice(0, 8).toUpperCase();
|
||||
console.error(`[${errorId}] ${event.request.method} ${event.url.pathname} -> ${status}`, error);
|
||||
return {
|
||||
message: status >= 500 ? 'An unexpected fault interrupted this operation. No further action was applied.' : message,
|
||||
errorId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
|
||||
type ErrorPresentation = { code: string; title: string; detail: string; action: 'home' | 'signin' | 'retry' };
|
||||
const presentations: Record<number, ErrorPresentation> = {
|
||||
400: { code: 'REQUEST REJECTED', title: 'INVALID TRANSMISSION', detail: 'The submitted request could not be processed. Review the supplied information and try again.', action: 'home' },
|
||||
401: { code: 'AUTHENTICATION REQUIRED', title: 'IDENTITY NOT VERIFIED', detail: 'This operation requires an authenticated SHD.build agent.', action: 'signin' },
|
||||
403: { code: 'ACCESS DENIED', title: 'CLEARANCE INSUFFICIENT', detail: 'Your current role does not authorize access to this location or operation.', action: 'home' },
|
||||
404: { code: 'SIGNAL LOST', title: 'LOCATION NOT FOUND', detail: 'ISAC could not resolve the requested location. It may have moved, been archived, or never existed.', action: 'home' },
|
||||
405: { code: 'METHOD REJECTED', title: 'PROTOCOL NOT SUPPORTED', detail: 'This location does not support the attempted operation.', action: 'home' },
|
||||
409: { code: 'DATA CONFLICT', title: 'STATE HAS CHANGED', detail: 'The requested change conflicts with newer or existing data. Reload the source and review it before trying again.', action: 'retry' },
|
||||
413: { code: 'PAYLOAD REJECTED', title: 'TRANSMISSION TOO LARGE', detail: 'The submitted payload exceeds the accepted size. Reduce the file or content size and try again.', action: 'home' },
|
||||
429: { code: 'RATE LIMITED', title: 'CHANNEL SATURATED', detail: 'Too many requests reached this node in a short period. Wait briefly before trying again.', action: 'retry' },
|
||||
500: { code: 'SYSTEM FAULT', title: 'ISAC PROCESS INTERRUPTED', detail: 'An unexpected fault interrupted this operation. No further action was applied.', action: 'retry' },
|
||||
502: { code: 'UPSTREAM FAULT', title: 'SERVICE LINK FAILED', detail: 'A required service returned an invalid response. Try the operation again shortly.', action: 'retry' },
|
||||
503: { code: 'SERVICE OFFLINE', title: 'NODE TEMPORARILY UNAVAILABLE', detail: 'This service is temporarily unavailable or not yet configured.', action: 'retry' },
|
||||
504: { code: 'LINK TIMEOUT', title: 'SERVICE RESPONSE EXPIRED', detail: 'A required service did not respond in time. Try the operation again shortly.', action: 'retry' }
|
||||
};
|
||||
const status = $derived(page.status || 500);
|
||||
const presentation = $derived(presentations[status] ?? { code: 'REQUEST FAILED', title: 'OPERATION INTERRUPTED', detail: 'ISAC could not complete the requested operation.', action: status >= 500 ? 'retry' : 'home' });
|
||||
const suppliedMessage = $derived(page.error?.message?.trim() ?? '');
|
||||
const safeMessage = $derived(suppliedMessage && suppliedMessage !== 'Internal Error' ? suppliedMessage : presentation.detail);
|
||||
const errorId = $derived(page.error && 'errorId' in page.error ? String(page.error.errorId ?? '') : '');
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{status} · {presentation.title} — SHD.build</title></svelte:head>
|
||||
|
||||
<main class="error-page">
|
||||
<section class="error-console" class:server-fault={status >= 500} aria-labelledby="error-title">
|
||||
<header><div><span>ISAC EXCEPTION REPORT</span><strong>{presentation.code}</strong></div><b>{status}</b></header>
|
||||
<div class="error-signal" aria-hidden="true"><i></i><span>{status}</span><i></i></div>
|
||||
<div class="error-copy"><p class="eyebrow"><span></span> SHD NETWORK RESPONSE</p><h1 id="error-title">{presentation.title}</h1><p>{safeMessage}</p></div>
|
||||
<dl>
|
||||
<div><dt>STATUS</dt><dd>HTTP {status}</dd></div>
|
||||
<div><dt>REQUEST PATH</dt><dd>{page.url.pathname}</dd></div>
|
||||
<div><dt>NODE</dt><dd>SHD.BUILD / ISAC</dd></div>
|
||||
{#if errorId}<div><dt>REFERENCE</dt><dd>{errorId}</dd></div>{/if}
|
||||
</dl>
|
||||
<footer>
|
||||
<div><i></i><span>{status >= 500 ? 'FAULT LOGGED · CORE SERVICES PROTECTED' : 'REQUEST TERMINATED · NO FURTHER ACTION'}</span></div>
|
||||
<nav aria-label="Error recovery">
|
||||
<button class="secondary" type="button" onclick={() => history.length > 1 ? history.back() : location.assign('/')}>GO BACK</button>
|
||||
{#if presentation.action === 'signin'}<a class="primary" href="/sign-in">SIGN IN →</a>
|
||||
{:else if presentation.action === 'retry'}<button class="primary" type="button" onclick={() => location.reload()}>RETRY REQUEST →</button>
|
||||
{:else}<a class="primary" href="/">RETURN HOME →</a>{/if}
|
||||
</nav>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
Reference in New Issue
Block a user