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
+
+
+
+
+
+