Add better error page

This commit is contained in:
Eduard Gert
2026-02-05 14:00:51 +01:00
parent 9b0387e7ee
commit 7504e718d7
12 changed files with 217 additions and 63 deletions

View File

@@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NetBird Service</title>
<meta name="robots" content="noindex, nofollow" />
<script type="module" crossorigin src="/assets/index-ClfM9m3s.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style-B1NSEbha.css">
<script type="module" crossorigin src="/assets/index-BWSM6sR5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style-IH-yd16d.css">
</head>
<body>
<!-- Go template variables injected here -->

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,66 @@
import { useEffect, useState } from "react";
import {BookText, RotateCw, Server, Globe, UserIcon, WaypointsIcon} from "lucide-react";
import { Title } from "@/components/Title";
import { Description } from "@/components/Description";
import Button from "@/components/Button";
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
import { StatusCard } from "@/components/StatusCard";
import type { ErrorData } from "@/data";
export function ErrorPage({ code, title, message, proxy = true, peer = true, destination = true, requestId }: ErrorData) {
useEffect(() => {
document.title = `${title} - NetBird Service`;
}, [title]);
const [timestamp] = useState(() => new Date().toISOString());
return (
<main className="flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto">
{/* Error Code */}
<div className="text-sm text-netbird font-normal font-mono mb-3 z-10 relative">
{code} {title === "Service Unavailable" ? "Bad Gateway" : title.split(" ").slice(0, 2).join(" ")}
</div>
{/* Title */}
<Title className="text-3xl!">{title}</Title>
{/* Description */}
<Description className="mt-2 mb-8 max-w-md">{message}</Description>
{/* Status Cards */}
<div className="hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative">
<StatusCard icon={UserIcon} label="You" line={false} />
<StatusCard icon={WaypointsIcon} label="Proxy" success={proxy} />
<StatusCard icon={Server} label="Routing Peer" success={peer} />
<StatusCard icon={Globe} label="Destination" success={destination} />
</div>
{/* Buttons */}
<div className="flex gap-3 justify-center items-center mb-6 z-10 relative">
<Button variant="primary" onClick={() => window.location.reload()}>
<RotateCw size={16} />
Refresh Page
</Button>
<Button
variant="secondary"
onClick={() => window.open("https://docs.netbird.io", "_blank")}
>
<BookText size={16} />
Documentation
</Button>
</div>
{/* Request Info */}
<div className="text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3">
<div>
<span className="text-nb-gray-400">REQUEST-ID:</span> {requestId}
</div>
<div>
<span className="text-nb-gray-400">TIMESTAMP:</span> {timestamp}
</div>
</div>
<PoweredByNetBird />
</main>
);
}

View File

@@ -0,0 +1,26 @@
import { X } from "lucide-react";
interface ConnectionLineProps {
success?: boolean;
}
export function ConnectionLine({ success = true }: ConnectionLineProps) {
if (success) {
return (
<div className="flex-1 flex items-center justify-center h-12 w-full px-5">
<div className="w-full border-t-2 border-dashed border-green-500" />
</div>
);
}
return (
<div className="flex-1 flex items-center justify-center h-12 min-w-10 px-5 relative">
<div className="w-full border-t-2 border-dashed border-nb-gray-900" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-8 h-8 rounded-full flex items-center justify-center">
<X size={18} className="text-netbird" />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import type { LucideIcon } from "lucide-react";
import { ConnectionLine } from "./ConnectionLine";
interface StatusCardProps {
icon: LucideIcon;
label: string;
detail?: string;
success?: boolean;
line?: boolean;
}
export function StatusCard({
icon: Icon,
label,
detail,
success = true,
line = true,
}: StatusCardProps) {
return (
<>
{line && <ConnectionLine success={success} />}
<div className="flex flex-col items-center gap-2">
<div className="w-14 h-14 rounded-md flex items-center justify-center from-nb-gray-940 to-nb-gray-930/70 bg-gradient-to-br border border-nb-gray-910">
<Icon size={20} className="text-nb-gray-200" />
</div>
<span className="text-sm text-nb-gray-200 font-normal mt-1">{label}</span>
<span className={`text-xs font-medium uppercase ${success ? "text-green-500" : "text-netbird"}`}>
{success ? "Connected" : "Unreachable"}
</span>
{detail && (
<span className="text-xs text-nb-gray-400 truncate text-center">
{detail}
</span>
)}
</div>
</>
);
}

View File

@@ -9,6 +9,10 @@ export interface ErrorData {
code: number
title: string
message: string
proxy?: boolean
peer?: boolean
destination?: boolean
requestId?: string
}
// Data injected by Go templates
@@ -25,5 +29,27 @@ declare global {
}
export function getData(): Data {
return window.__DATA__ ?? {}
const data = window.__DATA__ ?? {}
// Dev mode: allow ?page=error query param to preview error page
if (import.meta.env.DEV) {
const params = new URLSearchParams(window.location.search)
const page = params.get('page')
if (page === 'error') {
return {
...data,
page: 'error',
error: data.error ?? {
code: 503,
title: 'Service Unavailable',
message: 'The service you are trying to access is temporarily unavailable. Please try again later.',
proxy: true,
peer: false,
destination: false,
},
}
}
}
return data
}

View File

@@ -2,7 +2,7 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { ErrorPage } from './pages/ErrorPage.tsx'
import { ErrorPage } from './ErrorPage.tsx'
import { getData } from '@/data'
const data = getData()

View File

@@ -1,42 +0,0 @@
import { useEffect } from "react";
import { BookText, RotateCw } from "lucide-react";
import { Title } from "@/components/Title";
import { Description } from "@/components/Description";
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
import { Card } from "@/components/Card";
import Button from "@/components/Button";
import type { ErrorData } from "@/data";
export function ErrorPage({ code, title, message }: ErrorData) {
useEffect(() => {
document.title = `${title} - NetBird Service`;
}, [title]);
return (
<main className="flex flex-col items-center mt-40 px-4 max-w-xl mx-auto">
<Card className="text-center">
<div className="text-5xl font-bold text-nb-gray-200 mb-4">{code}</div>
<Title>{title}</Title>
<Description className="mt-2">{message}</Description>
<div className="mt-6 flex gap-3 justify-center">
<Button
variant="primary"
onClick={() => window.location.reload()}
>
<RotateCw size={16} />
Refresh Page
</Button>
<Button
variant="secondary"
onClick={() => window.open("https://docs.netbird.io", "_blank")}
>
<BookText size={16} />
Documentation
</Button>
</div>
</Card>
<PoweredByNetBird />
</main>
);
}

View File

@@ -108,14 +108,25 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, data any, statusCode ...i
w.Write(buf.Bytes())
}
// ErrorStatus represents the connection status for each component in the error page.
type ErrorStatus struct {
Proxy bool
Peer bool
Destination bool
}
// ServeErrorPage renders a user-friendly error page with the given details.
func ServeErrorPage(w http.ResponseWriter, r *http.Request, code int, title, message string) {
func ServeErrorPage(w http.ResponseWriter, r *http.Request, code int, title, message, requestID string, status ErrorStatus) {
ServeHTTP(w, r, map[string]any{
"page": "error",
"error": map[string]any{
"code": code,
"title": title,
"message": message,
"code": code,
"title": title,
"message": message,
"proxy": status.Proxy,
"peer": status.Peer,
"destination": status.Destination,
"requestId": requestID,
},
}, code)
}