feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
<?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] ?? '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user