docs: add dashboard error page final report and plan
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
# Dashboard Error Page Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Create a dedicated error page in the admin dashboard that shows full error context (status code, URL, message, stack trace, request params, user info, timestamp) with access restricted to super_admin users.
|
||||
|
||||
**Architecture:** Modify `Handler.php` to detect dashboard routes and render `Dashboard/DashboardError.vue` instead of `Error.vue`. The new component uses `DashboardLayout` and displays full error context as props from the backend.
|
||||
|
||||
**Tech Stack:** Laravel 10+, Vue 3 Composition API, Inertia.js, Tailwind CSS, Porto Architecture
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Code in English, comments in English
|
||||
- Composition API (`<script setup>`) for all Vue components
|
||||
- Use existing `DashboardLayout` component for page structure
|
||||
- Use `route()` (Ziggy) for URL generation
|
||||
- Follow Porto Architecture for backend changes
|
||||
- Stack trace and request params only shown in non-production environments
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Action | File | Responsibility |
|
||||
|--------|------|----------------|
|
||||
| Create | `resources/js/Pages/Dashboard/DashboardError.vue` | Error page with DashboardLayout, full context display |
|
||||
| Modify | `app/Exceptions/Handler.php` | Detect dashboard routes, render DashboardError.vue with context |
|
||||
| Modify | `app/Ship/Exceptions/Handler.php` | Same changes as App Handler (duplicate) |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create DashboardError.vue Component
|
||||
|
||||
**Covers:** S1 (Dashboard error page with full context)
|
||||
|
||||
**Files:**
|
||||
- Create: `resources/js/Pages/Dashboard/DashboardError.vue`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Props from Handler.php (status, message, url, method, stackTrace, requestParams, user, timestamp)
|
||||
- Produces: Rendered error page in DashboardLayout
|
||||
|
||||
- [ ] **Step 1: Create the Vue component**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Head>
|
||||
<title>Ошибка {{ status }} — Панель управления</title>
|
||||
</Head>
|
||||
|
||||
<DashboardLayout>
|
||||
<template #header-icon>
|
||||
<DashboardIcon name="exclamation-triangle" size="5" class="text-red-500" />
|
||||
</template>
|
||||
<template #header-title>Ошибка {{ status }}</template>
|
||||
<template #header-subtitle>{{ statusDescription }}</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Error Code Card -->
|
||||
<div class="bg-layer rounded-xl border border-layer-line p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-red-50 rounded-xl flex items-center justify-center">
|
||||
<span class="text-3xl font-bold text-red-600">{{ status }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground">{{ statusDescription }}</h2>
|
||||
<p class="text-sm text-muted-foreground-1">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Info Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- URL -->
|
||||
<div class="bg-layer rounded-xl border border-layer-line p-4">
|
||||
<h3 class="text-xs font-semibold text-muted-foreground-1 uppercase tracking-wide mb-2">
|
||||
URL запроса
|
||||
</h3>
|
||||
<p class="text-sm text-foreground font-mono break-all">{{ url }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Method -->
|
||||
<div class="bg-layer rounded-xl border border-layer-line p-4">
|
||||
<h3 class="text-xs font-semibold text-muted-foreground-1 uppercase tracking-wide mb-2">
|
||||
HTTP Метод
|
||||
</h3>
|
||||
<p class="text-sm text-foreground font-mono">{{ method }}</p>
|
||||
</div>
|
||||
|
||||
<!-- User -->
|
||||
<div class="bg-layer rounded-xl border border-layer-line p-4">
|
||||
<h3 class="text-xs font-semibold text-muted-foreground-1 uppercase tracking-wide mb-2">
|
||||
Пользователь
|
||||
</h3>
|
||||
<p v-if="user" class="text-sm text-foreground">{{ user.name }} ({{ user.email }})</p>
|
||||
<p v-else class="text-sm text-muted-foreground-2">Не авторизован</p>
|
||||
</div>
|
||||
|
||||
<!-- Timestamp -->
|
||||
<div class="bg-layer rounded-xl border border-layer-line p-4">
|
||||
<h3 class="text-xs font-semibold text-muted-foreground-1 uppercase tracking-wide mb-2">
|
||||
Время ошибки
|
||||
</h3>
|
||||
<p class="text-sm text-foreground">{{ formattedTimestamp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stack Trace (non-production only) -->
|
||||
<div v-if="stackTrace" class="bg-layer rounded-xl border border-layer-line p-4">
|
||||
<button
|
||||
@click="showStack = !showStack"
|
||||
class="flex items-center gap-2 text-sm font-semibold text-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<DashboardIcon
|
||||
:name="showStack ? 'chevron-down' : 'chevron-right'"
|
||||
size="4"
|
||||
/>
|
||||
Stack Trace
|
||||
</button>
|
||||
<div v-if="showStack" class="mt-4 overflow-x-auto">
|
||||
<pre class="text-xs text-muted-foreground-1 font-mono whitespace-pre-wrap bg-background-2 rounded-lg p-4">{{ stackTrace }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Params (non-production only) -->
|
||||
<div v-if="requestParams && Object.keys(requestParams).length" class="bg-layer rounded-xl border border-layer-line p-4">
|
||||
<button
|
||||
@click="showParams = !showParams"
|
||||
class="flex items-center gap-2 text-sm font-semibold text-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<DashboardIcon
|
||||
:name="showParams ? 'chevron-down' : 'chevron-right'"
|
||||
size="4"
|
||||
/>
|
||||
Параметры запроса
|
||||
</button>
|
||||
<div v-if="showParams" class="mt-4 overflow-x-auto">
|
||||
<pre class="text-xs text-muted-foreground-1 font-mono whitespace-pre-wrap bg-background-2 rounded-lg p-4">{{ JSON.stringify(requestParams, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="goBack"
|
||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-layer border border-layer-line rounded-lg text-sm font-medium text-foreground hover:bg-primary-50 hover:text-primary transition-all"
|
||||
>
|
||||
<DashboardIcon name="arrow-left" size="4" />
|
||||
Назад
|
||||
</button>
|
||||
<a
|
||||
:href="route('dashboard.index')"
|
||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-light transition-all"
|
||||
>
|
||||
<DashboardIcon name="home" size="4" />
|
||||
На главную
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import DashboardLayout from './Components/DashboardLayout.vue';
|
||||
import DashboardIcon from './Components/DashboardIcon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
status: { type: Number, required: true },
|
||||
message: { type: String, default: '' },
|
||||
url: { type: String, default: '' },
|
||||
method: { type: String, default: 'GET' },
|
||||
stackTrace: { type: String, default: null },
|
||||
requestParams: { type: Object, default: () => ({}) },
|
||||
user: { type: Object, default: null },
|
||||
timestamp: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const showStack = ref(false);
|
||||
const showParams = ref(false);
|
||||
|
||||
const statusDescription = computed(() => ({
|
||||
503: 'Сервис временно недоступен',
|
||||
500: 'Внутренняя ошибка сервера',
|
||||
404: 'Страница не найдена',
|
||||
403: 'Доступ запрещён',
|
||||
})[props.status] || 'Неизвестная ошибка');
|
||||
|
||||
const formattedTimestamp = computed(() => {
|
||||
if (!props.timestamp) return '—';
|
||||
return new Date(props.timestamp).toLocaleString('ru-RU', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'long',
|
||||
});
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
} else {
|
||||
window.location.href = route('dashboard.index');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify component structure**
|
||||
|
||||
Run: `grep -c "DashboardLayout" resources/js/Pages/Dashboard/DashboardError.vue`
|
||||
Expected: `2` (import + template usage)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add resources/js/Pages/Dashboard/DashboardError.vue
|
||||
git commit -m "feat(dashboard): add DashboardError.vue with full error context display"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Modify Exception Handler for Dashboard Routes
|
||||
|
||||
**Covers:** S1, S2 (Route detection, context gathering)
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/Exceptions/Handler.php`
|
||||
- Modify: `app/Ship/Exceptions/Handler.php`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Request object, exception, user roles
|
||||
- Produces: Inertia render with Dashboard/DashboardError.vue props
|
||||
|
||||
- [ ] **Step 1: Add helper method and modify render in App Handler**
|
||||
|
||||
Replace the `render` method in `app/Exceptions/Handler.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of exception types with their corresponding custom log levels.
|
||||
*
|
||||
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
|
||||
*/
|
||||
protected $levels = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array<int, class-string<\Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare exception for rendering.
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @return \Throwable
|
||||
*/
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
$response = parent::render($request, $e);
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
if (in_array($statusCode, [500, 503, 404, 403])) {
|
||||
if ($this->isDashboardRequest($request)) {
|
||||
return $this->renderDashboardError($request, $e, $statusCode);
|
||||
}
|
||||
|
||||
if (! app()->environment(['local', 'testing'])) {
|
||||
return Inertia::render('Error', ['status' => $statusCode])
|
||||
->toResponse($request)
|
||||
->setStatusCode($statusCode);
|
||||
}
|
||||
} elseif ($statusCode === 419) {
|
||||
return back()->with([
|
||||
'message' => 'The page expired, please try again.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is for a dashboard route.
|
||||
*/
|
||||
private function isDashboardRequest(Request $request): bool
|
||||
{
|
||||
return str_starts_with($request->path(), 'dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error page with full context for dashboard users.
|
||||
*/
|
||||
private function renderDashboardError(Request $request, Throwable $e, int $statusCode)
|
||||
{
|
||||
$user = $request->user();
|
||||
$isSuperAdmin = $user && $user->hasRole('super_admin');
|
||||
$isProduction = app()->environment('production');
|
||||
|
||||
$props = [
|
||||
'status' => $statusCode,
|
||||
'message' => $this->getErrorMessage($e, $statusCode),
|
||||
'url' => $request->fullUrl(),
|
||||
'method' => $request->method(),
|
||||
'user' => $user ? $user->only('id', 'name', 'email') : null,
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
// Only superadmins get stack trace and request params
|
||||
if ($isSuperAdmin && !$isProduction) {
|
||||
$props['stackTrace'] = $e->getTraceAsString();
|
||||
$props['requestParams'] = $request->except([
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'current_password',
|
||||
'_token',
|
||||
]);
|
||||
}
|
||||
|
||||
return Inertia::render('Dashboard/DashboardError', $props)
|
||||
->toResponse($request)
|
||||
->setStatusCode($statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly error message based on status code.
|
||||
*/
|
||||
private function getErrorMessage(Throwable $e, int $statusCode): string
|
||||
{
|
||||
$messages = [
|
||||
503 => 'Сервис временно недоступен. Попробуйте позже.',
|
||||
500 => 'Внутренняя ошибка сервера.',
|
||||
404 => 'Запрашиваемая страница не найдена.',
|
||||
403 => 'У вас нет доступа к этой странице.',
|
||||
];
|
||||
|
||||
return $messages[$statusCode] ?? 'Произошла неизвестная ошибка.';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Apply same changes to Ship Handler**
|
||||
|
||||
Replace the `render` method in `app/Ship/Exceptions/Handler.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Ship\Exceptions;
|
||||
|
||||
use App\Ship\Abstracts\Exceptions\Handler as AbstractHandler;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends AbstractHandler
|
||||
{
|
||||
/**
|
||||
* A list of exception types with their corresponding custom log levels.
|
||||
*
|
||||
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
|
||||
*/
|
||||
protected $levels = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array<int, class-string<\Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
$response = parent::render($request, $e);
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
if (in_array($statusCode, [500, 503, 404, 403])) {
|
||||
if ($this->isDashboardRequest($request)) {
|
||||
return $this->renderDashboardError($request, $e, $statusCode);
|
||||
}
|
||||
|
||||
if (! app()->environment(['local', 'testing'])) {
|
||||
return Inertia::render('Error', ['status' => $statusCode])
|
||||
->toResponse($request)
|
||||
->setStatusCode($statusCode);
|
||||
}
|
||||
} elseif ($statusCode === 419) {
|
||||
return back()->with([
|
||||
'message' => 'The page expired, please try again.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is for a dashboard route.
|
||||
*/
|
||||
private function isDashboardRequest(Request $request): bool
|
||||
{
|
||||
return str_starts_with($request->path(), 'dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error page with full context for dashboard users.
|
||||
*/
|
||||
private function renderDashboardError(Request $request, Throwable $e, int $statusCode)
|
||||
{
|
||||
$user = $request->user();
|
||||
$isSuperAdmin = $user && $user->hasRole('super_admin');
|
||||
$isProduction = app()->environment('production');
|
||||
|
||||
$props = [
|
||||
'status' => $statusCode,
|
||||
'message' => $this->getErrorMessage($e, $statusCode),
|
||||
'url' => $request->fullUrl(),
|
||||
'method' => $request->method(),
|
||||
'user' => $user ? $user->only('id', 'name', 'email') : null,
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
// Only superadmins get stack trace and request params
|
||||
if ($isSuperAdmin && !$isProduction) {
|
||||
$props['stackTrace'] = $e->getTraceAsString();
|
||||
$props['requestParams'] = $request->except([
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'current_password',
|
||||
'_token',
|
||||
]);
|
||||
}
|
||||
|
||||
return Inertia::render('Dashboard/DashboardError', $props)
|
||||
->toResponse($request)
|
||||
->setStatusCode($statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly error message based on status code.
|
||||
*/
|
||||
private function getErrorMessage(Throwable $e, int $statusCode): string
|
||||
{
|
||||
$messages = [
|
||||
503 => 'Сервис временно недоступен. Попробуйте позже.',
|
||||
500 => 'Внутренняя ошибка сервера.',
|
||||
404 => 'Запрашиваемая страница не найдена.',
|
||||
403 => 'У вас нет доступа к этой странице.',
|
||||
];
|
||||
|
||||
return $messages[$statusCode] ?? 'Произошла неизвестная ошибка.';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify PHP syntax**
|
||||
|
||||
Run: `docker exec ntspi-php php -l app/Exceptions/Handler.php && docker exec ntspi-php php -l app/Ship/Exceptions/Handler.php`
|
||||
Expected: No syntax errors
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/Exceptions/Handler.php app/Ship/Exceptions/Handler.php
|
||||
git commit -m "feat(dashboard): modify exception handlers to render DashboardError for dashboard routes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Verify End-to-End Flow
|
||||
|
||||
**Covers:** S1, S2, S3 (Full verification)
|
||||
|
||||
**Files:**
|
||||
- Verify: `resources/js/Pages/Dashboard/DashboardError.vue`
|
||||
- Verify: `app/Exceptions/Handler.php`
|
||||
- Verify: `app/Ship/Exceptions/Handler.php`
|
||||
|
||||
- [ ] **Step 1: Test 404 on dashboard route**
|
||||
|
||||
Navigate to: `http://localhost/dashboard/nonexistent-page`
|
||||
Expected: DashboardError page with status 404, dashboard layout visible
|
||||
|
||||
- [ ] **Step 2: Test 403 on dashboard route (non-superadmin)**
|
||||
|
||||
Login as non-superadmin user, navigate to restricted route
|
||||
Expected: DashboardError page with status 403, no stack trace or params
|
||||
|
||||
- [ ] **Step 3: Test 403 on dashboard route (superadmin)**
|
||||
|
||||
Login as superadmin, trigger 403
|
||||
Expected: DashboardError page with status 403, stack trace and params visible
|
||||
|
||||
- [ ] **Step 4: Test public site error (unchanged)**
|
||||
|
||||
Navigate to non-existent public page
|
||||
Expected: Original Error.vue with public layout
|
||||
|
||||
- [ ] **Step 5: Final commit (if any fixes needed)**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix(dashboard): address review feedback on error page"
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
feature: dashboard-error-page
|
||||
status: delivered
|
||||
specs: []
|
||||
plans:
|
||||
- docs/compose/plans/2026-07-05-dashboard-error-page.md
|
||||
branch: master
|
||||
commits: a4fef0b..df6110a
|
||||
---
|
||||
|
||||
# Dashboard Error Page — Final Report
|
||||
|
||||
## What Was Built
|
||||
|
||||
A dedicated error page for the admin dashboard that displays full error context when HTTP errors (403, 404, 500, 503) occur on dashboard routes. Previously, dashboard errors redirected to a generic error page using the public site layout (navbar + footer), which was confusing for admin users.
|
||||
|
||||
The new `DashboardError.vue` component renders within the Dashboard layout (sidebar + header) and shows:
|
||||
- Error status code and description
|
||||
- Request URL and HTTP method
|
||||
- Current user info (if authenticated)
|
||||
- Timestamp of the error
|
||||
- Stack trace and request parameters (visible only to super_admin users in non-production environments)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `resources/js/Pages/Dashboard/DashboardError.vue` | Vue error page with full context display |
|
||||
| `app/Exceptions/Handler.php` | Detects dashboard routes, renders DashboardError |
|
||||
| `app/Ship/Exceptions/Handler.php` | Duplicate handler (Porto architecture) |
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Exception thrown
|
||||
↓
|
||||
Handler.php render()
|
||||
↓
|
||||
isDashboardRequest()? ──Yes──→ renderDashboardError()
|
||||
│ ↓
|
||||
│ Check user role
|
||||
│ ↓
|
||||
│ Build props (status, message, url, etc.)
|
||||
│ ↓
|
||||
│ Inertia::render('Dashboard/DashboardError', $props)
|
||||
│
|
||||
└──No──→ Original Error.vue (public layout)
|
||||
```
|
||||
|
||||
### Access Control
|
||||
|
||||
- **All users**: See error status, description, URL, method, user info, timestamp
|
||||
- **Super_admin only** (non-production): Additionally see stack trace and request parameters
|
||||
- **Production**: Stack trace and params never shown (regardless of role)
|
||||
|
||||
## Usage
|
||||
|
||||
When a dashboard route returns an HTTP error (403, 404, 500, 503), the user is automatically redirected to the DashboardError page with full context. No configuration required.
|
||||
|
||||
**Example scenarios:**
|
||||
- User navigates to `/dashboard/nonexistent-page` → 404 with URL info
|
||||
- User accesses restricted route without permission → 403 with user info
|
||||
- Server error occurs → 500 with timestamp
|
||||
|
||||
**Navigation:**
|
||||
- "Назад" button: Returns to previous page (or dashboard index if no history)
|
||||
- "На главную" button: Navigates to dashboard index
|
||||
|
||||
## Verification
|
||||
|
||||
1. **404 test**: `curl -s -o /dev/null -w "%{http_code}" http://localhost/dashboard/nonexistent-page` → Returns `404`
|
||||
2. **Props verification**: Confirmed `status=404`, `message`, `url`, `method`, `timestamp` all present in Inertia page data
|
||||
3. **Stack trace hidden**: Confirmed stack trace and params not included for unauthenticated users
|
||||
4. **Public site unchanged**: Public site errors still render with original `Error.vue` layout
|
||||
5. **PHP syntax**: Both Handler files pass `php -l` syntax check
|
||||
|
||||
## Journey Log
|
||||
|
||||
- [lesson] Vite manifest requires rebuild after adding new Vue components — `npm run build` needed before testing
|
||||
- [lesson] Porto architecture has duplicate exception handlers (`app/Exceptions/` and `app/Ship/Exceptions/`) — both must be updated identically
|
||||
Reference in New Issue
Block a user