# 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 (`
```
- [ ] **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
, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array
*/
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
, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array
*/
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"
```