diff --git a/docs/compose/plans/2026-07-05-dashboard-error-page.md b/docs/compose/plans/2026-07-05-dashboard-error-page.md new file mode 100644 index 0000000..f3be5ce --- /dev/null +++ b/docs/compose/plans/2026-07-05-dashboard-error-page.md @@ -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 (` +``` + +- [ ] **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" +``` diff --git a/docs/compose/reports/dashboard-error-page.md b/docs/compose/reports/dashboard-error-page.md new file mode 100644 index 0000000..1b9be53 --- /dev/null +++ b/docs/compose/reports/dashboard-error-page.md @@ -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