withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', channels: __DIR__.'/../routes/channels.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { // GLOBAL MIDDLEWARE $middleware->append(\App\Http\Middleware\GzipCompression::class); // REGISTER ALIAS MIDDLEWARE $middleware->alias([ 'role' => RoleMiddleware::class, 'permission' => PermissionMiddleware::class, 'role_or_permission' => RoleOrPermissionMiddleware::class, // CUSTOM MIDDLEWARE (CEK PERMISSION AKTIF) 'active-permission' => CheckActivePermission::class, 'password-expiry' => \App\Http\Middleware\PasswordExpiryMiddleware::class, 'check-legal' => \App\Http\Middleware\CheckLegalAgreement::class, 'mobile-guard' => \App\Http\Middleware\MobileMaintenanceMiddleware::class, 'tab-permission' => \App\Http\Middleware\CheckTabPermission::class, 'menu-permission' => \App\Http\Middleware\CheckMenuPermission::class, ]); $middleware->web(append: [ \App\Http\Middleware\SecurityHeaders::class, \App\Http\Middleware\IpAccessControl::class, 'password-expiry', 'check-legal', ]); }) ->withSchedule(function (\Illuminate\Console\Scheduling\Schedule $schedule): void { // Dikelola di routes/console.php }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->report(function (\Throwable $e) { // --- AI SELF HEALING INTERCEPTOR --- if (!($e instanceof \Symfony\Component\HttpKernel\Exception\HttpException)) { try { // Re-entrancy guard: if the AI healer itself is currently running, // never re-enter (prevents infinite loops if healing trips an error). if (defined('AI_HEALER_RUNNING')) { return; } $isAiHealingEnabled = false; $isAiProviderEnabled = false; try { $isAiHealingEnabled = app(\App\Services\SystemConfig\SystemConfigService::class) ->get('ai_healing_enabled', false); $isAiProviderEnabled = app(\App\Services\SystemConfig\SystemConfigService::class) ->get('ai_enabled', false); } catch (\Throwable $t) {} if (!$isAiHealingEnabled || !$isAiProviderEnabled) { // Healer or Provider disabled — fall through to Sentry below. } else { // Skip exceptions originating *from* the healing pipeline itself // OR from critical files we never want the healer to touch. $traceStr = $e->getFile() . $e->getTraceAsString(); $msg = (string) $e->getMessage(); $isInternalHealerError = str_contains($traceStr, 'AiHealerJob') || str_contains($traceStr, 'AiHealingLog') || str_contains($traceStr, 'AiSelfHealingController') || str_contains($traceStr, 'app/Services/AI/') // Self-exclusion: never let the healer try to edit these files. || str_contains($msg, 'ai-self-healing.blade.php') || str_contains($msg, 'navigation.blade.php') || str_contains($msg, 'layouts/app.blade.php') || str_contains($traceStr, 'ai-self-healing.blade.php') || str_contains($traceStr, 'navigation.blade.php'); if (!$isInternalHealerError) { // Dedup: same error_type already pending OR diagnosing. // Diagnosing logs older than 5 min are considered stuck and reset to pending. \App\Models\AiHealingLog::where('error_type', get_class($e)) ->where('status', 'diagnosing') ->where('updated_at', '<', now()->subMinutes(5)) ->update(['status' => 'pending', 'action_taken' => 'Auto-reset from stuck diagnosing state']); $duplicate = \App\Models\AiHealingLog::where('error_type', get_class($e)) ->whereIn('status', ['pending', 'diagnosing']) ->where('created_at', '>=', now()->subMinutes(2)) ->exists(); if (!$duplicate) { // --- CIRCUIT BREAKER --- $maxPerHour = (int) app(\App\Services\SystemConfig\SystemConfigService::class) ->get('ai_healing_max_attempts_per_hour', 5); $recentCount = \App\Models\AiHealingLog::where('created_at', '>', now()->subHour()) ->whereIn('status', ['resolved', 'pending', 'diagnosing']) ->count(); if ($recentCount >= $maxPerHour) { // Circuit breaker tripped! Only log to standard Laravel log, don't trigger AI. \Illuminate\Support\Facades\Log::warning("AI Healer Circuit Breaker tripped. Total fixes in last hour: {$recentCount}. Threshold: {$maxPerHour}"); return; } define('AI_HEALER_RUNNING', true); $log = \App\Models\AiHealingLog::create([ 'error_type' => get_class($e), 'error_message' => $e->getMessage(), 'stack_trace' => $e->getTraceAsString(), 'status' => 'pending', ]); try { \App\Jobs\AiHealerJob::dispatchSync( $log->id, $e->getFile(), $e->getLine() ); } catch (\Throwable $inner) { $log->update([ 'status' => 'failed', 'ai_diagnosis' => 'Healer dispatch crashed: ' . $inner->getMessage(), 'action_taken' => 'Pipeline error', ]); } } } } } catch (\Throwable $err) { // Silently ignore — never let the interceptor break error reporting. } } if (app()->bound('sentry') && app()->environment('production', 'staging')) { app('sentry')->captureException($e); } }); }) ->create();