From 98359069496e9679b02b4dcc007aaf45ea7b30b4 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 15 Nov 2025 21:11:07 +0100 Subject: [PATCH] added nice "error 500" pages, fixed core system migration --- app/Exceptions/Handler.php | 90 +++++++++++++++ ...01_01_000000_create_core_system_tables.php | 8 +- resources/lang/de/marketing.php | 24 ++++ resources/lang/en/marketing.php | 24 ++++ resources/views/errors/500.blade.php | 2 + resources/views/errors/503.blade.php | 2 + resources/views/errors/server.blade.php | 109 ++++++++++++++++++ 7 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 resources/views/errors/500.blade.php create mode 100644 resources/views/errors/503.blade.php create mode 100644 resources/views/errors/server.blade.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b8cd258..1cd9727 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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; + } } diff --git a/database/migrations/0001_01_01_000000_create_core_system_tables.php b/database/migrations/0001_01_01_000000_create_core_system_tables.php index 6bb4fa5..0ae0da7 100644 --- a/database/migrations/0001_01_01_000000_create_core_system_tables.php +++ b/database/migrations/0001_01_01_000000_create_core_system_tables.php @@ -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'); } -}; \ No newline at end of file +}; diff --git a/resources/lang/de/marketing.php b/resources/lang/de/marketing.php index e74d6b3..6afd8e2 100644 --- a/resources/lang/de/marketing.php +++ b/resources/lang/de/marketing.php @@ -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', diff --git a/resources/lang/en/marketing.php b/resources/lang/en/marketing.php index c6412e4..029fad5 100644 --- a/resources/lang/en/marketing.php +++ b/resources/lang/en/marketing.php @@ -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' => 'Where’s the database? We can’t reach it right now – it’s 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 can’t 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', diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php new file mode 100644 index 0000000..9b3bf0f --- /dev/null +++ b/resources/views/errors/500.blade.php @@ -0,0 +1,2 @@ +@php($statusCode = 500) +@include('errors.server', ['statusCode' => $statusCode]) diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php new file mode 100644 index 0000000..15bcd31 --- /dev/null +++ b/resources/views/errors/503.blade.php @@ -0,0 +1,2 @@ +@php($statusCode = 503) +@include('errors.server', ['statusCode' => $statusCode]) diff --git a/resources/views/errors/server.blade.php b/resources/views/errors/server.blade.php new file mode 100644 index 0000000..5dbae99 --- /dev/null +++ b/resources/views/errors/server.blade.php @@ -0,0 +1,109 @@ + + + + + + {{ __('marketing.server_error.title', [], app()->getLocale()) }} · Fotospiel + @vite(['resources/css/app.css']) + + + @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 + +
+
+
+
+
+
+ +
+
+ {{ $statusCode }} · {{ __('marketing.server_error.title') }} +
+ +

+ {{ __('marketing.server_error.subtitle') }} +

+ +

+ {{ __('marketing.server_error.status_label') }}: + {{ $statusCode }} +

+ +

+ {{ __('marketing.server_error.description') }} +

+ +
+
+

+ {{ __('marketing.server_error.tip_heading') }} +

+
+
+
    + @foreach($tips as $index => $tip) +
  • + + {{ $index + 1 }} + + {{ $tip }} +
  • + @endforeach +
+
+ + @if(! empty($hintMessage)) +
+

+ {{ __('marketing.server_error.hint_label') }} +

+

+ {{ $hintMessage }} +

+
+ @endif + + +
+
+ +