authorize('view ai self-healing'); $grouped = $systemConfig->grouped(); $settings = $grouped['ai_healing'] ?? []; $aiCfg = $grouped['ai_config'] ?? []; $logs = AiHealingLog::latest()->take(100)->get(); $stats = $this->buildStats(); $topExceptions = AiHealingLog::select('error_type', DB::raw('COUNT(*) as total')) ->groupBy('error_type') ->orderByDesc('total') ->take(5) ->get() ->map(fn ($r) => [ 'type' => class_basename($r->error_type), 'fqcn' => $r->error_type, 'count' => (int) $r->getAttribute('total'), ]) ->all(); $timeline = $this->build24hTimeline(); $lastIncident = AiHealingLog::latest()->value('created_at'); $providerInfo = [ 'enabled' => (bool) ($aiCfg['ai_enabled'] ?? false), 'provider' => strtoupper($aiCfg['ai_provider'] ?? 'gpt'), 'has_key' => $this->resolveProviderKeyPresent($aiCfg), ]; return view('pages.system_settings.ai-self-healing', compact( 'logs', 'settings', 'stats', 'topExceptions', 'timeline', 'lastIncident', 'providerInfo' )); } public function update(Request $request, SystemConfigService $systemConfig) { $this->authorize('manage ai self-healing'); $input = [ 'ai_healing_enabled' => $request->has('ai_healing_enabled'), 'ai_healing_allow_cache' => $request->has('ai_healing_allow_cache'), 'ai_healing_allow_queue' => $request->has('ai_healing_allow_queue'), 'ai_healing_allow_maintenance' => $request->has('ai_healing_allow_maintenance'), 'ai_healing_allow_db' => $request->has('ai_healing_allow_db'), 'ai_healing_max_attempts_per_hour' => $request->input('ai_healing_max_attempts_per_hour', 5), ]; $systemConfig->update($input, [], auth()->id(), $request); if ($request->wantsJson() || $request->ajax()) { return response()->json([ 'success' => true, 'message' => 'AI Healing configuration saved successfully.', 'settings' => $input, ]); } return redirect()->back()->with('success', 'AI Healing configuration saved successfully.'); } public function show($id) { $this->authorize('view ai self-healing'); $log = AiHealingLog::findOrFail($id); // Parse the action_taken marker for human-readable display + rollback info $action = (string) $log->action_taken; $actionPretty = $action; $rollbackInfo = null; if (str_starts_with($action, 'CODE_EDIT|')) { $parts = explode('|', $action, 3); $filePath = $parts[1] ?? ''; $backupPath = $parts[2] ?? ''; $backupExists = $backupPath && file_exists($backupPath); $actionPretty = 'Code edit applied in ' . basename($filePath) . ($backupExists ? ' (backup available)' : ' (backup missing)'); $rollbackInfo = [ 'available' => $backupExists, 'file' => basename($filePath), 'backup_file' => basename($backupPath), ]; } elseif (str_starts_with($action, 'ROLLED_BACK|')) { $parts = explode('|', $action, 3); $filePath = $parts[1] ?? ''; $actionPretty = 'Rolled back: ' . basename($filePath) . ' restored from backup'; } return response()->json([ 'success' => true, 'data' => [ 'id' => $log->id, 'error_type' => $log->error_type, 'error_message' => $log->error_message, 'stack_trace' => $log->stack_trace, 'ai_diagnosis' => $log->ai_diagnosis, 'action_taken' => $actionPretty, 'action_raw' => $action, 'rollback' => $rollbackInfo, 'status' => $log->status, 'original_code' => $log->original_code, 'fixed_code' => $log->fixed_code, 'created_at' => optional($log->created_at)->format('d M Y, H:i:s'), 'relative_time' => optional($log->created_at)->diffForHumans(), ], ]); } public function clearLogs() { $this->authorize('manage ai self-healing'); AiHealingLog::truncate(); return response()->json([ 'success' => true, 'message' => 'All AI Healing logs have been cleared successfully.', ]); } public function retry($id) { $this->authorize('manage ai self-healing'); $log = AiHealingLog::findOrFail($id); if (!\in_array($log->status, ['failed', 'diagnosing', 'pending'], true)) { return response()->json([ 'success' => false, 'message' => 'Only failed, pending, or stuck-diagnosing logs can be retried.', ], 422); } $log->update([ 'status' => 'pending', 'action_taken' => 'Retry queued by ' . (auth()->user()->name ?? 'system'), ]); try { // Synchronous dispatch — matches the original interceptor behaviour // so the user sees the new outcome immediately on refresh. \App\Jobs\AiHealerJob::dispatchSync($log->id); } catch (\Throwable $e) { $log->update([ 'status' => 'failed', 'ai_diagnosis' => 'Retry crashed: ' . $e->getMessage(), 'action_taken' => 'Retry pipeline error', ]); return response()->json([ 'success' => false, 'message' => 'Retry failed: ' . $e->getMessage(), ], 500); } return response()->json([ 'success' => true, 'message' => 'Healing re-attempted. Status: ' . $log->fresh()->status, 'status' => $log->fresh()->status, ]); } public function rollback($id) { $this->authorize('manage ai self-healing'); $log = AiHealingLog::findOrFail($id); $action = (string) $log->action_taken; if (!str_starts_with($action, 'CODE_EDIT|')) { return response()->json([ 'success' => false, 'message' => 'This incident has no code-edit backup to roll back.', ], 422); } $parts = explode('|', $action, 3); $filePath = $parts[1] ?? null; $backupPath = $parts[2] ?? null; if (!$filePath || !$backupPath || !file_exists($backupPath)) { return response()->json([ 'success' => false, 'message' => 'Backup file not found. It may have been deleted manually.', ], 404); } if (!is_writable($filePath)) { return response()->json([ 'success' => false, 'message' => 'Target file is not writable.', ], 422); } $backupContent = file_get_contents($backupPath); file_put_contents($filePath, $backupContent); \Illuminate\Support\Facades\Artisan::call('optimize:clear'); $log->update([ 'action_taken' => 'ROLLED_BACK|' . $filePath . '|from ' . basename($backupPath), 'ai_diagnosis' => '[rolled back by ' . (auth()->user()->name ?? 'system') . '] ' . $log->ai_diagnosis, ]); return response()->json([ 'success' => true, 'message' => 'Rollback completed. ' . basename($filePath) . ' restored from backup.', ]); } public function stats() { $this->authorize('view ai self-healing'); return response()->json($this->buildStats() + [ 'timeline' => $this->build24hTimeline(), 'last_incident' => optional(AiHealingLog::latest()->value('created_at'))->diffForHumans() ?? 'No incidents', ]); } private function buildStats(): array { $total = AiHealingLog::count(); $resolved = AiHealingLog::where('status', 'resolved')->count(); $failed = AiHealingLog::where('status', 'failed')->count(); $pending = AiHealingLog::whereIn('status', ['pending', 'diagnosing'])->count(); return [ 'total' => $total, 'resolved' => $resolved, 'failed' => $failed, 'pending' => $pending, 'rate' => $total > 0 ? round(($resolved / $total) * 100) : 0, 'last_24h' => AiHealingLog::where('created_at', '>=', now()->subDay())->count(), ]; } private function build24hTimeline(): array { $from = now()->subHours(23)->startOfHour(); $buckets = []; for ($i = 23; $i >= 0; $i--) { $hour = now()->subHours($i)->startOfHour(); $buckets[$hour->format('Y-m-d H')] = [ 'label' => $hour->format('H:00'), 'count' => 0, ]; } AiHealingLog::where('created_at', '>=', $from) ->select('created_at') ->orderBy('created_at') ->get() ->each(function ($row) use (&$buckets) { $key = Carbon::parse($row->created_at)->startOfHour()->format('Y-m-d H'); if (isset($buckets[$key])) { $buckets[$key]['count']++; } }); return array_values($buckets); } private function resolveProviderKeyPresent(array $aiCfg): bool { $provider = $aiCfg['ai_provider'] ?? 'gpt'; $keyMap = [ 'gpt' => 'ai_gpt_key', 'gemini' => 'ai_gemini_key', 'claude' => 'ai_claude_key', 'deepseek' => 'ai_deepseek_key', 'grok' => 'ai_grok_key', 'mistral' => 'ai_mistral_key', 'openrouter' => 'ai_openrouter_key', ]; $field = $keyMap[$provider] ?? null; return $field !== null && !empty($aiCfg[$field] ?? ''); } }