From e0bf3b05abc17e4790ca3ae532c357c85507c37f Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 25 Jul 2026 18:52:39 -0400 Subject: [PATCH] Add SHD themed error handling --- docs/DESIGN_SYSTEM.md | 18 +++++++++++++++ src/app.css | 3 +++ src/app.d.ts | 5 +++- src/error.html | 12 ++++++++++ src/hooks.server.ts | 12 +++++++++- src/routes/+error.svelte | 49 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/error.html create mode 100644 src/routes/+error.svelte diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 909a392..25a1066 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -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`. diff --git a/src/app.css b/src/app.css index 7fb4acf..10a2c46 100644 --- a/src/app.css +++ b/src/app.css @@ -58,6 +58,9 @@ .admin-layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:12px;align-items:start}.admin-nav{position:sticky;top:88px;display:flex;flex-direction:column;background:#111415;border:1px solid #2d3233;padding:8px 0}.admin-nav a{color:#9ca0a1;border-bottom:1px solid #292d2f;padding:13px 14px;font-size:10px}.admin-nav a.active{color:#f6a821;border-left:2px solid #f6a821}.table{overflow:auto}.table-row{display:grid;grid-template-columns:1.5fr repeat(3,1fr) .35fr;gap:10px;align-items:center;border-top:1px solid #2b3031;padding:11px 14px;font-size:11px;min-width:700px}.table-row.head{font-size:8px;color:#6f7577}.table-row button,.card button:not(.primary):not(.secondary){background:none;border:0;border-bottom:1px solid transparent;color:#f6a821;font-size:9px;cursor:pointer}.table-row button:hover{border-bottom-color:#f6a821}.primary,.secondary,.danger{display:inline-block;text-decoration:none}.auth-page{min-height:calc(100vh - 76px)}hr{border:0;border-top:1px solid #303536;margin:26px 0}button:disabled,fieldset[disabled]{cursor:wait}button:not(:disabled):active,a.primary:active,a.secondary:active{transform:translateY(2px) scale(.985);filter:brightness(.9)} @media(max-width:900px){.site-header nav{display:none}.cards,.builder,.admin-layout,.form-grid{grid-template-columns:1fr}.slots{grid-template-columns:1fr}.admin-nav{position:static;display:grid;grid-template-columns:1fr 1fr}.page-head{align-items:flex-start;gap:22px}}@media(max-width:560px){.page-head{flex-direction:column}.site-header .account>a{display:none}.cards{grid-template-columns:1fr}} +/* SHD routed exception console */ +.error-page{min-height:calc(100vh - var(--shd-header-height));display:grid;place-items:center;padding:clamp(20px,5vw,72px);background:radial-gradient(circle at 50% 45%,#282116 0,transparent 38%)}.error-console{width:min(900px,100%);border:1px solid #4a4337;border-top:2px solid var(--shd-orange);background:linear-gradient(135deg,#141718,#0d1011);box-shadow:0 24px 70px #0008}.error-console.server-fault{border-top-color:var(--shd-danger)}.error-console>header{position:static;height:65px;padding:0 18px;border-bottom:1px solid var(--shd-border);background:#101314}.error-console>header>div{display:grid;gap:4px}.error-console>header span{color:var(--shd-text-muted);font-size:7px;font-weight:900;letter-spacing:1.4px}.error-console>header strong{color:var(--shd-orange);font-size:10px;letter-spacing:1px}.error-console.server-fault>header strong,.error-console.server-fault>header>b{color:var(--shd-danger)}.error-console>header>b{color:var(--shd-orange);font-size:32px;letter-spacing:-1px}.error-signal{height:78px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:15px;padding:0 clamp(18px,6vw,60px);color:var(--shd-orange)}.server-fault .error-signal{color:var(--shd-danger)}.error-signal i{height:1px;background:linear-gradient(90deg,transparent,currentColor)}.error-signal i:last-child{background:linear-gradient(90deg,currentColor,transparent)}.error-signal span{width:44px;height:44px;display:grid;place-items:center;border:1px solid currentColor;font-size:10px;font-weight:900;box-shadow:0 0 18px #f6a82122}.error-copy{padding:0 clamp(24px,7vw,78px) 34px;text-align:center}.error-copy .eyebrow{margin-bottom:15px}.error-copy h1{max-width:720px;margin:0 auto 17px;font-size:clamp(38px,6vw,72px);line-height:.9;letter-spacing:-2.5px}.error-copy>p:last-child{max-width:650px;margin:0 auto;color:var(--shd-text-muted);font:15px/1.6 Georgia,serif}.error-console dl{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:0;border-top:1px solid var(--shd-border);border-bottom:1px solid var(--shd-border)}.error-console dl>div{min-width:0;display:grid;gap:5px;padding:12px 15px;border-left:1px solid var(--shd-border)}.error-console dl>div:first-child{border-left:0}.error-console dt{color:var(--shd-text-dim);font-size:6px;font-weight:900;letter-spacing:1px}.error-console dd{overflow:hidden;margin:0;color:#c7cbcc;font-size:8px;font-weight:900;letter-spacing:.5px;text-overflow:ellipsis;white-space:nowrap}.error-console>footer{min-height:68px;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:12px 16px}.error-console>footer>div{display:flex;align-items:center;gap:8px;color:var(--shd-text-dim);font-size:6px;font-weight:900;letter-spacing:.8px}.error-console>footer>div i{width:6px;height:6px;background:var(--shd-success);box-shadow:0 0 7px var(--shd-success)}.error-console.server-fault>footer>div i{background:var(--shd-danger);box-shadow:0 0 7px var(--shd-danger)}.error-console>footer nav{height:auto;margin:0;display:flex;gap:8px}.error-console>footer .primary,.error-console>footer .secondary{min-height:40px;display:inline-grid;place-items:center;padding:0 15px}@media(max-width:700px){.error-page{place-items:start center}.error-console dl{grid-template-columns:1fr 1fr}.error-console dl>div:nth-child(3){border-left:0}.error-console dl>div:nth-child(n+3){border-top:1px solid var(--shd-border)}.error-console>footer{align-items:stretch;flex-direction:column}.error-console>footer nav{width:100%}.error-console>footer nav>*{flex:1}.error-copy h1{letter-spacing:-1.5px}}@media(max-width:430px){.error-console dl{grid-template-columns:1fr}.error-console dl>div{border-left:0;border-top:1px solid var(--shd-border)}.error-console dl>div:first-child{border-top:0}.error-console>footer nav{flex-direction:column}} + /* SHD admin operations dashboard */ .admin-page{width:100%;min-height:calc(100vh - 76px);padding:18px clamp(14px,2vw,32px) 40px;background:linear-gradient(135deg,#101314aa 0,transparent 38%)}.admin-command{height:auto;min-height:76px;position:relative;top:auto;display:flex;justify-content:space-between;align-items:center;padding:0 0 16px;background:transparent;border-bottom:1px solid #303536}.command-title h1{margin:4px 0 2px;font-size:27px;letter-spacing:1px}.command-title>p:last-child{margin:0;color:#697073;font-size:9px;letter-spacing:1.1px}.command-status{display:flex;gap:7px}.command-status>div{position:relative;min-width:105px;height:47px;padding:9px 12px;background:#111415;border:1px solid #303536;display:grid;align-content:center}.command-status span{font-size:7px;letter-spacing:1.3px;color:#6f7678}.command-status strong{font-size:10px;letter-spacing:1px;margin-top:3px}.command-status i{position:absolute;right:9px;top:9px;width:6px;height:6px;border-radius:50%;background:#68bd82;box-shadow:0 0 8px #68bd82}.admin-dashboard{display:grid;grid-template-columns:210px minmax(0,1fr);gap:12px;margin-top:12px}.admin-sidebar{position:sticky;top:88px;min-height:520px;height:calc(100vh - 118px);display:flex;flex-direction:column;background:#0f1213;border:1px solid #2c3132}.sidebar-label{height:43px;padding:10px 12px;border-bottom:1px solid #2c3132;display:flex;justify-content:space-between;align-items:center}.sidebar-label span,.sidebar-label b{font-size:7px;letter-spacing:1.3px}.sidebar-label span{color:#8d9395}.sidebar-label b{color:#505759}.admin-sidebar nav{display:flex;flex-direction:column;margin:0;height:auto}.admin-sidebar nav a{display:grid;grid-template-columns:33px 1fr auto;align-items:center;min-height:49px;padding:0 12px 0 0;color:#858c8e;border:0;border-bottom:1px solid #272c2d;font-size:10px}.admin-sidebar nav a small{height:100%;display:grid;place-items:center;background:#141819;color:#535a5c;font-size:7px;letter-spacing:.6px}.admin-sidebar nav a span{padding-left:11px}.admin-sidebar nav a b{font-size:17px;color:#f6a821}.admin-sidebar nav a em{font-style:normal;font-size:6px;color:#555c5e;letter-spacing:1px}.admin-sidebar nav a:hover,.admin-sidebar nav a.active{color:#fff;background:#151819}.admin-sidebar nav a.active{box-shadow:inset 2px 0 #f6a821}.admin-sidebar nav a.active small{background:#2b2111;color:#f6a821}.admin-sidebar nav a.pending{opacity:.5;pointer-events:none}.sidebar-foot{margin-top:auto;padding:14px;border-top:1px solid #2c3132;display:grid}.sidebar-foot span{font-size:7px;color:#5e6668;letter-spacing:1.3px}.sidebar-foot strong{font-size:14px;color:#f6a821;margin:4px 0}.sidebar-foot small{font-size:8px;color:#596164}.admin-content{min-width:0}.module-toolbar{min-height:63px;display:flex;align-items:center;justify-content:space-between;padding:0 2px 12px;border-bottom:1px solid #303536}.module-toolbar h2{font-size:19px;letter-spacing:.4px;margin:3px 0}.module-toolbar p{margin:0}.module-toolbar>div>p:last-child{font-size:9px;color:#71787a}.module-toolbar>div:last-child{display:flex;gap:8px}.module-toolbar .primary,.module-toolbar .secondary{height:40px;padding:0 15px}.module-kpis{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;margin:10px 0}.module-kpis>div{position:relative;min-height:70px;padding:11px 13px;background:#111415;border:1px solid #2d3233;border-top:2px solid #3a4041;display:grid}.module-kpis>div:first-child{border-top-color:#f6a821}.module-kpis span{font-size:7px;letter-spacing:1.3px;color:#737a7c}.module-kpis strong{font-size:23px;line-height:1.1;margin:3px 0}.module-kpis small{font-size:7px;color:#555d5f}.dashboard-editor{margin:10px 0;background:#131617;border:1px solid #4a5051;border-top:2px solid #f6a821;padding:14px 16px}.editor-title{display:flex;justify-content:space-between;align-items:start;padding-bottom:9px;border-bottom:1px solid #303536}.editor-title h3{font-size:16px;margin:3px 0}.editor-title button{border:0;background:transparent;color:#7e8587;font-size:24px;line-height:1}.dashboard-editor .form-grid{grid-template-columns:repeat(5,minmax(0,1fr));gap:9px}.dashboard-editor .field{margin:8px 0}.dashboard-editor .field input,.dashboard-editor .field select,.dashboard-editor .field textarea{padding:8px 9px;margin-top:5px;font-size:10px}.range-field>div,.uptime-field>div{display:grid;grid-template-columns:1fr 1fr;gap:5px}.dashboard-table{border:1px solid #2d3233;background:#111415;overflow:auto}.dashboard-row{min-width:820px;display:grid;grid-template-columns:1.5fr 1fr .75fr .85fr .6fr .3fr;align-items:center;gap:10px;min-height:46px;padding:7px 13px;border-top:1px solid #272c2d;font-size:10px}.dashboard-row:hover:not(.table-heading){background:#15191a}.dashboard-row>div{display:grid;gap:3px}.dashboard-row>div small{font-size:7px;color:#60686a}.dashboard-row>span{width:max-content}.dashboard-row>button{justify-self:end;border:0;border-bottom:1px solid transparent;background:transparent;color:#f6a821;font-size:8px;font-weight:bold;padding:5px;cursor:pointer}.dashboard-row>button:hover{border-color:#f6a821}.table-heading{min-height:32px;border-top:0;background:#0d1011;color:#697174;font-size:7px;letter-spacing:1.2px}.state-dot{font-size:7px;letter-spacing:.8px;color:#75c08c}.state-dot:before{content:"";display:inline-block;width:5px;height:5px;margin-right:6px;border-radius:50%;background:currentColor;box-shadow:0 0 6px currentColor}.dashboard-row.archived{opacity:.45}.type-offensive,.type-defensive{padding:4px 6px;border:1px solid}.type-offensive{color:#df8069;border-color:#70463c}.type-defensive{color:#70b9d6;border-color:#365e6e}.tier-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:10px}.tier-card{min-width:0;padding:8px 11px 11px;border:1px solid #343a3b;background:#101314}.tier-card legend{padding:0 7px;color:#f6a821;font-size:8px;font-weight:bold;letter-spacing:1.3px}.trigger-list{max-height:130px;overflow:auto;border:1px solid #292f30}.trigger-list>div{display:grid;grid-template-columns:1fr auto;align-items:center;min-height:33px;padding:4px 7px;border-top:1px solid #252a2b;background:#121516}.trigger-list label{display:flex;align-items:center;gap:7px;font-size:9px}.trigger-list input{width:auto;margin:0;accent-color:#f6a821}.trigger-list button,.effects-head button{border:0;background:transparent;color:#f6a821;font-size:7px}.effects-head{display:flex;justify-content:space-between;align-items:center;margin-top:10px}.talent-effect{display:grid;grid-template-columns:1fr 90px 27px;gap:5px;margin-top:5px}.talent-effect select,.talent-effect input{min-width:0;padding:7px;margin:0;font-size:9px}.talent-effect button{border:1px solid #563734;background:#1d1212;color:#da746c}.talent-identity{grid-template-columns:2fr 1fr!important}.uptime-field>span{display:flex;align-items:center;gap:7px}.uptime-field>span input{width:auto;margin:0;accent-color:#f6a821}.script-note{align-self:end;margin:8px 0;padding:9px;border:1px dashed #394041;display:grid}.script-note b{font-size:7px;color:#8e9597;letter-spacing:1.2px}.script-note span{font-size:8px;color:#5d6567;margin-top:4px}.configured{color:#79bd8d;font-size:8px} @media(max-width:900px){.admin-command{align-items:flex-start}.command-status>div:nth-child(n+2){display:none}.admin-dashboard{grid-template-columns:1fr}.admin-sidebar{position:static;height:auto;min-height:0}.admin-sidebar nav{display:grid;grid-template-columns:1fr 1fr}.sidebar-foot{display:none}.module-kpis{grid-template-columns:1fr 1fr}.dashboard-editor .form-grid{grid-template-columns:1fr 1fr}.tier-grid{grid-template-columns:1fr}}@media(max-width:560px){.admin-command,.module-toolbar{flex-direction:column;align-items:flex-start;gap:12px}.module-toolbar>div:last-child{width:100%}.module-toolbar button{flex:1}.module-kpis{grid-template-columns:1fr 1fr}.dashboard-editor .form-grid{grid-template-columns:1fr}.talent-identity{grid-template-columns:1fr!important}} diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..4933f0a 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -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 {} diff --git a/src/error.html b/src/error.html new file mode 100644 index 0000000..18ed73d --- /dev/null +++ b/src/error.html @@ -0,0 +1,12 @@ + + + + + + %sveltekit.status% · SHD.build system fault + + +
ISAC FALLBACK CHANNELCORE SHELL INTERRUPTED
%sveltekit.status%
SHD NETWORK RESPONSE

SYSTEM INTERFACE UNAVAILABLE

%sveltekit.error.message%

RETURN TO SHD.BUILD →
FAULT ISOLATED · CORE SERVICES PROTECTED
+ diff --git a/src/hooks.server.ts b/src/hooks.server.ts index ef69bb6..9a90c7e 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -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 + }; +}; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000..21b4d6b --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,49 @@ + + +{status} · {presentation.title} — SHD.build + +
+
= 500} aria-labelledby="error-title"> +
ISAC EXCEPTION REPORT{presentation.code}
{status}
+ +

SHD NETWORK RESPONSE

{presentation.title}

{safeMessage}

+
+
STATUS
HTTP {status}
+
REQUEST PATH
{page.url.pathname}
+
NODE
SHD.BUILD / ISAC
+ {#if errorId}
REFERENCE
{errorId}
{/if} +
+ +
+