Files

289 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\SystemSettings;
use App\Http\Controllers\Controller;
use App\Models\AiHealingLog;
use App\Services\SystemConfig\SystemConfigService;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class AiSelfHealingController extends Controller
{
public function index(SystemConfigService $systemConfig)
{
$this->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] ?? '');
}
}