Add SHD themed error handling

This commit is contained in:
JB
2026-07-25 18:52:39 -04:00
parent 92cda8a576
commit e0bf3b05ab
6 changed files with 97 additions and 2 deletions
+18
View File
@@ -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`.
+3
View File
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -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 {}
+12
View File
@@ -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
View File
@@ -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
};
};
+49
View File
@@ -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>