added nice "error 500" pages, fixed core system migration

This commit is contained in:
Codex Agent
2025-11-15 21:11:07 +01:00
parent b56bd62cd0
commit 9835906949
7 changed files with 255 additions and 4 deletions

View File

@@ -3,8 +3,15 @@
namespace App\Exceptions;
use App\Support\ApiError;
use Illuminate\Database\Connectors\ConnectionException as DatabaseConnectionException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Client\ConnectionException as HttpClientConnectionException;
use Illuminate\Queue\InvalidQueueException;
use Illuminate\Queue\MaxAttemptsExceededException;
use Illuminate\Validation\ValidationException;
use League\Flysystem\FilesystemException;
use PDOException;
use Throwable;
class Handler extends ExceptionHandler
@@ -63,6 +70,12 @@ class Handler extends ExceptionHandler
}
}
if (! $request->expectsJson() && ! $request->inertia()) {
if ($hintKey = $this->resolveServerErrorHint($e)) {
$request->attributes->set('serverErrorHint', __($hintKey, [], app()->getLocale()));
}
}
return parent::render($request, $e);
}
@@ -74,4 +87,81 @@ class Handler extends ExceptionHandler
return 'Your request could not be processed. Please verify the details and try again.';
}
private function resolveServerErrorHint(Throwable $throwable): ?string
{
$status = $this->isHttpException($throwable)
? $this->toHttpException($throwable)->getStatusCode()
: 500;
if ($status < 500) {
return null;
}
if ($this->exceptionChainContains($throwable, [
QueryException::class,
DatabaseConnectionException::class,
PDOException::class,
'Doctrine\\DBAL\\Exception',
])) {
return 'marketing.server_error.hints.database';
}
if ($this->exceptionChainContains($throwable, [
FilesystemException::class,
'League\\Flysystem\\UnableToWriteFile',
'League\\Flysystem\\UnableToCreateDirectory',
'League\\Flysystem\\UnableToCheckExistence',
])) {
return 'marketing.server_error.hints.storage';
}
if ($this->exceptionChainContains($throwable, [
InvalidQueueException::class,
MaxAttemptsExceededException::class,
'RedisException',
'Predis\\Connection\\ConnectionException',
])) {
return 'marketing.server_error.hints.queue';
}
if ($this->exceptionChainContains($throwable, [
HttpClientConnectionException::class,
'GuzzleHttp\\Exception\\ConnectException',
'Psr\\Http\\Client\\NetworkExceptionInterface',
'Symfony\\Component\\HttpClient\\Exception\\TransportExceptionInterface',
])) {
return 'marketing.server_error.hints.network';
}
return 'marketing.server_error.hints.generic';
}
private function exceptionChainContains(Throwable $throwable, array $classNames): bool
{
do {
foreach ($classNames as $className) {
if ($this->throwableIsInstanceOf($throwable, $className)) {
return true;
}
}
$throwable = $throwable->getPrevious();
} while ($throwable);
return false;
}
private function throwableIsInstanceOf(Throwable $throwable, string $className): bool
{
if ($className === '') {
return false;
}
if (! class_exists($className) && ! interface_exists($className)) {
return false;
}
return $throwable instanceof $className;
}
}

View File

@@ -18,12 +18,12 @@ return new class extends Migration
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role')->default('super_admin')->after('password');
$table->string('role')->default('super_admin');
$table->text('two_factor_secret')->nullable();
$table->text('two_factor_recovery_codes')->nullable();
$table->timestamp('two_factor_confirmed_at')->nullable();
$table->string('username', 32)->nullable()->unique()->after('email');
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'))->after('role');
$table->string('username', 32)->nullable()->unique();
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'));
$table->rememberToken();
$table->timestamps();
$table->string('status')->default('active');
@@ -119,4 +119,4 @@ return new class extends Migration
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('users');
}
};
};

View File

@@ -180,6 +180,30 @@ return [
'cta_contact' => 'Kontakt aufnehmen',
'requested_path_label' => 'Angefragter Pfad',
],
'server_error' => [
'title' => 'Unerwarteter Fehler',
'subtitle' => 'Unser Team wurde bereits informiert.',
'description' => 'Während wir den Fehler beheben, kannst du die folgenden Schritte ausprobieren oder zu einem sicheren Bereich der App zurückkehren.',
'tip_heading' => 'Das kannst du tun',
'tips' => [
'Lade die Seite nach einer kurzen Pause neu.',
'Wechsle zur Startseite oder den Paketen und stöbere weiter.',
'Melde dich bei uns, wenn der Fehler bestehen bleibt oder du dringend Hilfe brauchst.',
],
'tip_fallback' => 'Bitte versuche es gleich noch einmal.',
'status_label' => 'Statuscode',
'hint_label' => 'Was ist passiert?',
'cta_home' => 'Zur Startseite',
'cta_packages' => 'Pakete entdecken',
'cta_contact' => 'Support kontaktieren',
'hints' => [
'database' => 'Wo ist die Datenbank? Wir erreichen sie gerade nicht vermutlich ein Verbindungs- oder Zugriffsproblem.',
'storage' => 'Der Upload-Speicher ist vorübergehend nicht erreichbar oder schreibgeschützt. Versuche es gleich noch einmal.',
'queue' => 'Unsere Hintergrundprozesse (Queue/Redis) sind kurz offline. Wir starten sie neu.',
'network' => 'Wir können einen externen Dienst nicht erreichen. Möglich ist eine Firewall oder ein Ausfall beim Provider.',
'generic' => 'Es ist ein unerwarteter Fehler aufgetreten. Wir kümmern uns bereits darum.',
],
],
'success' => [
'title' => 'Erfolgreich',
'verify_email' => 'E-Mail verifizieren',

View File

@@ -180,6 +180,30 @@ return [
'cta_contact' => 'Get in touch',
'requested_path_label' => 'Requested path',
],
'server_error' => [
'title' => 'We ran into an issue',
'subtitle' => 'Our team has already been notified.',
'description' => 'While we work on the fix, you can follow the tips below or head back to a safe spot in the app.',
'tip_heading' => 'In the meantime',
'tips' => [
'Reload the page after a short break.',
'Visit the homepage or packages overview to keep browsing.',
'Contact us if the issue persists or you need urgent help.',
],
'tip_fallback' => 'Please try again in a moment.',
'status_label' => 'Status code',
'hint_label' => 'What happened?',
'cta_home' => 'Back to homepage',
'cta_packages' => 'Explore packages',
'cta_contact' => 'Contact support',
'hints' => [
'database' => 'Wheres the database? We cant reach it right now its usually a credentials or network issue.',
'storage' => 'Uploads storage is temporarily unavailable or read-only. Nothing was lost; please retry shortly.',
'queue' => 'Our background workers (queue/Redis) are offline for a moment. We are restarting them.',
'network' => 'We cant connect to an upstream service. It might be a firewall or provider outage.',
'generic' => 'We hit an unexpected error and are already investigating.',
],
],
'success' => [
'title' => 'Success',
'verify_email' => 'Verify Email',

View File

@@ -0,0 +1,2 @@
@php($statusCode = 500)
@include('errors.server', ['statusCode' => $statusCode])

View File

@@ -0,0 +1,2 @@
@php($statusCode = 503)
@include('errors.server', ['statusCode' => $statusCode])

View File

@@ -0,0 +1,109 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ __('marketing.server_error.title', [], app()->getLocale()) }} · Fotospiel</title>
@vite(['resources/css/app.css'])
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-900 via-gray-900 to-black text-white antialiased">
@php
$statusCode = $statusCode ?? null;
if (! $statusCode && isset($exception) && method_exists($exception, 'getStatusCode')) {
$statusCode = $exception->getStatusCode();
}
$statusCode = is_numeric($statusCode) ? (int) $statusCode : 500;
if ($statusCode < 500 || $statusCode >= 600) {
$statusCode = 500;
}
$tips = trans('marketing.server_error.tips');
if (! is_array($tips) || empty($tips)) {
$tips = [__(
'marketing.server_error.tip_fallback',
[],
app()->getLocale()
)];
}
$hintMessage = $hint ?? request()->attributes->get('serverErrorHint');
@endphp
<main class="relative mx-auto flex min-h-screen max-w-5xl flex-col items-center justify-center px-6 py-16 text-center">
<div class="pointer-events-none absolute inset-0">
<div class="absolute -left-32 -top-32 h-96 w-96 rounded-full bg-amber-500/30 blur-3xl"></div>
<div class="absolute right-0 top-1/3 h-80 w-80 rounded-full bg-rose-500/20 blur-3xl"></div>
<div class="absolute bottom-0 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-blue-500/20 blur-3xl"></div>
</div>
<section class="relative z-10 w-full space-y-8 rounded-3xl border border-white/10 bg-white/5 p-10 shadow-2xl shadow-black/40 backdrop-blur">
<div class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-amber-200 shadow-lg shadow-amber-500/30">
{{ $statusCode }} · {{ __('marketing.server_error.title') }}
</div>
<h1 class="text-balance text-4xl font-bold leading-tight text-white sm:text-5xl md:text-6xl">
{{ __('marketing.server_error.subtitle') }}
</h1>
<p class="text-sm text-white/60">
{{ __('marketing.server_error.status_label') }}:
<span class="font-mono">{{ $statusCode }}</span>
</p>
<p class="mx-auto max-w-2xl text-lg text-white/70 sm:text-xl">
{{ __('marketing.server_error.description') }}
</p>
<div class="flex flex-col gap-6 rounded-3xl border border-white/10 bg-white/10 p-8 text-left shadow-lg shadow-black/30 backdrop-blur md:flex-row md:gap-8">
<div class="md:w-1/3">
<h2 class="text-lg font-semibold uppercase tracking-widest text-amber-200">
{{ __('marketing.server_error.tip_heading') }}
</h2>
<div class="mt-4 h-1 w-16 rounded-full bg-amber-400"></div>
</div>
<ul class="space-y-3 text-base text-white/80 md:w-2/3">
@foreach($tips as $index => $tip)
<li class="flex items-start gap-3">
<span class="mt-1 flex h-6 w-6 flex-none items-center justify-center rounded-full bg-white/10 text-xs font-semibold text-amber-200">
{{ $index + 1 }}
</span>
<span>{{ $tip }}</span>
</li>
@endforeach
</ul>
</div>
@if(! empty($hintMessage))
<div class="rounded-3xl border border-amber-300/40 bg-amber-500/10 p-6 text-left text-white/80 shadow-inner shadow-amber-900/30">
<p class="text-xs font-semibold uppercase tracking-widest text-amber-200">
{{ __('marketing.server_error.hint_label') }}
</p>
<p class="mt-3 text-base text-white">
{{ $hintMessage }}
</p>
</div>
@endif
<div class="flex flex-col gap-3 sm:flex-row sm:justify-center">
<a
href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}"
class="inline-flex items-center justify-center rounded-full bg-white px-6 py-3 font-medium text-gray-900 shadow-lg shadow-amber-500/30 transition hover:-translate-y-1 hover:bg-amber-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{{ __('marketing.server_error.cta_home') }}
</a>
<a
href="{{ route('packages', ['locale' => app()->getLocale()]) }}"
class="inline-flex items-center justify-center rounded-full border border-white/40 px-6 py-3 font-medium text-white transition hover:-translate-y-1 hover:border-white hover:bg-white/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{{ __('marketing.server_error.cta_packages') }}
</a>
<a
href="{{ app()->getLocale() === 'en' ? route('marketing.contact', ['locale' => app()->getLocale()]) : route('kontakt', ['locale' => app()->getLocale()]) }}"
class="inline-flex items-center justify-center rounded-full border border-transparent bg-gradient-to-r from-amber-500 to-rose-500 px-6 py-3 font-medium text-white shadow-lg shadow-rose-500/30 transition hover:-translate-y-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{{ __('marketing.server_error.cta_contact') }}
</a>
</div>
</section>
</main>
</body>
</html>