Files
biiproject-kit-v1/bootstrap/app.php
T

163 lines
8.2 KiB
PHP

<?php
/**
* ============================================================
* @project biiproject
* @author Andika Debi Putra
* @email andikadebiputra@gmail.com
* @website https://biiproject.com
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
* @version 1.0.0
* @created 2026-05-01
* ============================================================
*/
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Spatie\Permission\Middleware\RoleMiddleware;
use Spatie\Permission\Middleware\PermissionMiddleware;
use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;
use App\Http\Middleware\CheckActivePermission;
return Application::configure(basePath: dirname(__DIR__))
->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();