user()->can('view health and logs')) { abort(403); } $stats = $this->monitor->getAll(); return view('pages.system_settings.system-monitoring', compact('stats')); } public function getStats(Request $request) { if (! $request->user()->can('view health and logs')) { return response()->json([]); } return response()->json($this->monitor->getAll()); } public function logsDataTable(Request $request) { if (! $request->user()->can('view health and logs')) { return DataTable::response($request, 0, 0, []); } $logFile = storage_path('logs/laravel.log'); if (! File::exists($logFile)) { return DataTable::response($request, 0, 0, []); } // Optimization: Don't read the whole file into memory. Use tail to get last 2000 lines. // This prevents memory exhaustion on large log files. $handle = shell_exec('tail -n 2000 '.escapeshellarg($logFile)); $lines = $handle ? explode("\n", trim($handle)) : []; $logs = []; $currentLog = null; foreach ($lines as $line) { // Pattern for standard Laravel logs: [YYYY-MM-DD HH:MM:SS] env.LEVEL: message if (preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.*?)\.(.*?): (.*)/', $line, $matches)) { if ($currentLog) { $logs[] = $currentLog; } $currentLog = [ 'timestamp' => $matches[1], 'env' => $matches[2], 'level' => strtolower($matches[3]), 'message' => $matches[4], ]; } else { if ($currentLog) { $currentLog['message'] .= "\n".$line; } } } if ($currentLog) { $logs[] = $currentLog; } $logs = array_reverse($logs); // Newest first $recordsTotal = count($logs); // Filter $searchValue = DataTable::globalSearch($request); if ($searchValue) { $searchValue = strtolower($searchValue); $logs = array_filter($logs, function ($log) use ($searchValue) { return str_contains(strtolower($log['timestamp']), $searchValue) || str_contains(strtolower($log['level']), $searchValue) || str_contains(strtolower($log['message']), $searchValue); }); } $recordsFiltered = count($logs); // Sorting [$orderIndex, $orderDir] = DataTable::order($request, 0, 'desc'); $sortKey = match ($orderIndex) { 0 => 'timestamp', 1 => 'level', 2 => 'message', default => 'timestamp' }; usort($logs, function ($a, $b) use ($sortKey, $orderDir) { if ($orderDir === 'asc') { return $a[$sortKey] <=> $b[$sortKey]; } return $b[$sortKey] <=> $a[$sortKey]; }); // Pagination $data = array_slice($logs, DataTable::start($request), DataTable::length($request)); $rows = array_map(function ($log) { $levelClass = match ($log['level']) { 'error', 'critical', 'alert', 'emergency' => 'text-bg-danger', 'warning' => 'text-bg-warning', 'notice', 'info' => 'text-bg-info', 'debug' => 'text-bg-light border', default => 'text-bg-secondary' }; return [ '
'.e($log['timestamp']).'
', ''.strtoupper(e($log['level'])).'', '
'.e(str_replace("\n", ' ', substr($log['message'], 0, 300))).'
', '
', ]; }, $data); return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows); } public function downloadLogs() { $logFile = storage_path('logs/laravel.log'); if (! File::exists($logFile)) { return back()->with('error', 'Log file not found.'); } return Response::download($logFile, 'laravel-'.now()->format('Y-m-d').'.log'); } public function clearLogs() { $logFile = storage_path('logs/laravel.log'); File::put($logFile, ''); return response()->json(['success' => true, 'message' => 'Logs cleared successfully.']); } /** * SAP RFC LOGS DATATABLE */ public function sapLogsDataTable(Request $request) { $possiblePaths = [ base_path('dev_rfc.trc'), public_path('dev_rfc.trc'), storage_path('logs/dev_rfc.trc'), ]; $logFile = null; foreach ($possiblePaths as $path) { if (File::exists($path)) { $logFile = $path; break; } } if (! $logFile) { return DataTable::response($request, 0, 0, []); } // Optimization: Use tail for large SAP logs $handle = shell_exec('tail -c 500000 '.escapeshellarg($logFile)); // Read last 500KB $content = $handle ?: ''; $blocks = explode('****', $content); $logs = []; foreach ($blocks as $block) { $block = trim($block); if (empty($block)) { continue; } $timestamp = 'N/A'; if (preg_match('/at (\d{8} \d{6})/', $block, $matches)) { $timestamp = Carbon::createFromFormat('Ymd His', $matches[1])->format('Y-m-d H:i:s'); } // Extract a clean summary (e.g. "Trace file opened", "RFC Call", etc.) $summary = 'General RFC Event'; if (preg_match('/Trace file opened/i', $block)) { $summary = 'SYSTEM: Trace Initialized'; } elseif (preg_match('/RFC\s+Call\s+to\s+function\s+([\w_]+)/i', $block, $m)) { $summary = 'CALL: '.$m[1]; } elseif (preg_match('/RFC\s+Call/i', $block)) { $summary = 'NETWORK: RFC Call Initiated'; } elseif (preg_match('/Error/i', $block)) { $summary = 'ERROR: RFC Fault'; } elseif (preg_match('/Connection/i', $block)) { $summary = 'CONN: State Changed'; } $logs[] = [ 'timestamp' => $timestamp, 'summary' => $summary, 'message' => $block, ]; } $logs = array_reverse($logs); $recordsTotal = count($logs); // Search $searchValue = DataTable::globalSearch($request); if ($searchValue) { $searchValue = strtolower($searchValue); $logs = array_filter($logs, function ($log) use ($searchValue) { return str_contains(strtolower($log['timestamp']), $searchValue) || str_contains(strtolower($log['summary']), $searchValue) || str_contains(strtolower($log['message']), $searchValue); }); } $recordsFiltered = count($logs); $data = array_slice($logs, DataTable::start($request), DataTable::length($request)); $rows = array_map(function ($log) { $summaryClass = 'text-primary'; if (str_contains($log['summary'], 'ERROR')) { $summaryClass = 'text-danger fw-bold'; } if (str_contains($log['summary'], 'SYSTEM')) { $summaryClass = 'text-success'; } if (str_contains($log['summary'], 'CALL:')) { $summaryClass = 'text-info fw-bold'; } return [ '
'.e($log['timestamp']).'
', '
'.e($log['summary']).'
', '
', ]; }, $data); return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows); } public function clearSapLogs() { $possiblePaths = [ base_path('dev_rfc.trc'), public_path('dev_rfc.trc'), storage_path('logs/dev_rfc.trc'), ]; foreach ($possiblePaths as $path) { if (File::exists($path)) { File::put($path, ''); } } return response()->json(['success' => true, 'message' => 'SAP RFC logs cleared successfully.']); } public function downloadSapLogs() { $possiblePaths = [ base_path('dev_rfc.trc'), public_path('dev_rfc.trc'), storage_path('logs/dev_rfc.trc'), ]; foreach ($possiblePaths as $path) { if (File::exists($path)) { return Response::download($path, 'dev_rfc-'.now()->format('Y-m-d').'.trc'); } } return back()->with('error', 'SAP log file not found.'); } /** * MOBILE LOGS DATATABLE */ public function mobileLogsDataTable(Request $request) { $logFile = storage_path('logs/mobile.log'); if (! File::exists($logFile)) { return DataTable::response($request, 0, 0, []); } // Optimization: Get last 2000 lines only $handle = shell_exec('tail -n 2000 '.escapeshellarg($logFile)); $lines = $handle ? explode("\n", trim($handle)) : []; $logs = []; $currentLog = null; foreach ($lines as $line) { if (preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (.*?)\.(.*?): (.*)/', $line, $matches)) { if ($currentLog) { $logs[] = $currentLog; } $currentLog = [ 'timestamp' => $matches[1], 'level' => strtolower($matches[3]), 'message' => $matches[4], ]; } else { if ($currentLog) { $currentLog['message'] .= "\n".$line; } } } if ($currentLog) { $logs[] = $currentLog; } $logs = array_reverse($logs); $recordsTotal = count($logs); // Search $searchValue = DataTable::globalSearch($request); if ($searchValue) { $searchValue = strtolower($searchValue); $logs = array_filter($logs, function ($log) use ($searchValue) { return str_contains(strtolower($log['timestamp']), $searchValue) || str_contains(strtolower($log['message']), $searchValue); }); } $recordsFiltered = count($logs); $data = array_slice($logs, DataTable::start($request), DataTable::length($request)); $rows = array_map(function ($log) { $levelClass = match ($log['level']) { 'error', 'critical' => 'text-bg-danger', 'warning' => 'text-bg-warning', default => 'text-bg-info' }; // Try to extract user info from context if it was logged as JSON $userInfo = 'Guest'; if (preg_match('/Context: (\{.*\})/s', $log['message'], $matches)) { $ctx = json_decode($matches[1], true); if (isset($ctx['user_id'])) { $userInfo = 'ID: '.$ctx['user_id']; } } return [ '
'.e($log['timestamp']).'
', ''.strtoupper(e($log['level'])).'', '
'.e($userInfo).'
', '
'.e(str_replace("\n", ' ', substr($log['message'], 0, 300))).'
', '
', ]; }, $data); return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows); } public function clearMobileLogs() { File::put(storage_path('logs/mobile.log'), ''); return response()->json(['success' => true, 'message' => 'Mobile logs cleared successfully.']); } public function downloadMobileLogs() { $logFile = storage_path('logs/mobile.log'); if (! File::exists($logFile)) { return back()->with('error', 'Mobile log file not found.'); } return Response::download($logFile, 'mobile-'.now()->format('Y-m-d').'.log'); } /** * UNIFIED BACKGROUND JOBS DATATABLE */ public function backgroundJobsDataTable(Request $request) { $canManage = $request->user()->can('manage health and logs'); $pendingJobs = DB::table('jobs')->get()->map(function ($j) { $j->status = 'PENDING'; $j->timestamp = Carbon::createFromTimestamp($j->available_at)->format('Y-m-d H:i:s'); $j->exception = ''; return $j; }); $failedJobs = DB::table('failed_jobs')->get()->map(function ($j) { $j->status = 'FAILED'; $j->timestamp = is_numeric($j->failed_at) ? Carbon::createFromTimestamp($j->failed_at)->format('Y-m-d H:i:s') : $j->failed_at; return $j; }); $allJobs = collect($pendingJobs)->merge($failedJobs); $recordsTotal = $allJobs->count(); // Search $searchValue = DataTable::globalSearch($request); if ($searchValue) { $searchValue = strtolower($searchValue); $allJobs = $allJobs->filter(function ($j) use ($searchValue) { return str_contains(strtolower($j->id), $searchValue) || str_contains(strtolower($j->queue), $searchValue) || str_contains(strtolower($j->status), $searchValue) || str_contains(strtolower($j->payload), $searchValue) || str_contains(strtolower($j->exception), $searchValue); }); } $recordsFiltered = $allJobs->count(); // Sort [$orderIndex, $orderDir] = DataTable::order($request, 0, 'desc'); $sortKey = match ($orderIndex) { 0 => 'timestamp', 1 => 'status', 2 => 'queue', default => 'timestamp' }; if ($orderDir === 'asc') { $allJobs = $allJobs->sortBy($sortKey); } else { $allJobs = $allJobs->sortByDesc($sortKey); } // Paginate $allJobs = $allJobs->slice(DataTable::start($request), DataTable::length($request)); $rows = $allJobs->map(function ($job) use ($canManage) { $payload = json_decode($job->payload); $displayName = $payload->displayName ?? 'Unknown Job'; $statusBadge = $job->status === 'PENDING' ? 'PENDING' : 'FAILED'; $details = ''; if ($job->status === 'FAILED') { $exceptionShort = substr($job->exception, 0, 100); if (preg_match('/^([^\s:]+):/', $job->exception, $matches)) { $exceptionClass = explode('\\', $matches[1]); $exceptionClass = end($exceptionClass); } else { $exceptionClass = 'Error'; } $details = '
'.e($exceptionClass).': '.e(str_replace("\n", ' ', $exceptionShort)).'...
'; } else { $details = '
'.e($displayName).'
'; } // 🚀 HUMAN-READABLE DESCRIPTION MAPPING (TEXT ONLY) $taskDescription = match (true) { str_contains($displayName, 'SystemManagementNotification') => 'Mengirim Notifikasi Manajemen Sistem', str_contains($displayName, 'BroadcastEvent') => 'Penyiaran Data Real-time (Websocket)', str_contains($displayName, 'Backup') => 'Menjalankan Pencadangan Sistem', str_contains($displayName, 'Cleanup') => 'Pembersihan Data Berkala', str_contains($displayName, 'Sync') => 'Sinkronisasi Data Eksternal', default => 'Tugas Sistem Latar Belakang' }; $row = [ '
'.e($job->timestamp).'
', $statusBadge, ''.strtoupper(e($job->queue)).'', $details, '
'.$taskDescription.'
', ]; if ($canManage) { $actions = '
'; if ($job->status === 'FAILED') { $actions .= ''; $actions .= ''; $actions .= ''; } else { // Pending jobs cannot be deleted individually as per user request $actions .= ''; } $actions .= '
'; $row[] = $actions; } return $row; })->values()->all(); return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows); } /** * CLEAR ALL PENDING AND FAILED JOBS */ public function clearFailedJobs() { // 1. Clear Failed Jobs Artisan::call('queue:flush'); // 2. Clear Pending Jobs DB::table('jobs')->delete(); return response()->json(['success' => true, 'message' => 'All pending and failed jobs have been cleared.']); } /** * RETRY FAILED JOB(S) */ public function retryFailedJob(Request $request, $id = null) { if ($id) { Artisan::call('queue:retry', ['id' => [$id]]); $msg = "Job #{$id} has been pushed back to the queue."; } else { Artisan::call('queue:retry', ['id' => ['all']]); $msg = 'All failed jobs have been pushed back to the queue.'; } return response()->json(['success' => true, 'message' => $msg]); } /** * DELETE SINGLE FAILED JOB */ public function deleteFailedJob($id) { DB::table('failed_jobs')->where('id', $id)->delete(); return response()->json(['success' => true, 'message' => 'Failed job deleted successfully.']); } /** * DELETE SINGLE PENDING JOB */ public function deletePendingJob($id) { DB::table('jobs')->where('id', $id)->delete(); return response()->json(['success' => true, 'message' => 'Pending job deleted successfully.']); } /** * DOWNLOAD BACKGROUND JOBS (Pending & Failed) */ public function downloadBackgroundJobs(Request $request) { $format = $request->get('format', 'csv'); $delimiter = $format === 'excel' ? "\t" : ','; $filename = 'background-jobs-'.now()->format('Y-m-d-His').'.'.($format === 'excel' ? 'xls' : 'csv'); return response()->streamDownload(function () use ($delimiter) { $file = fopen('php://output', 'w'); // Add BOM for Excel UTF-8 support fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); $columns = ['ID', 'Type', 'Status', 'Queue', 'Timestamp', 'Message (Payload/Exception)']; fputcsv($file, $columns, $delimiter); // Pending Jobs DB::table('jobs')->orderBy('available_at', 'desc')->chunk(100, function ($jobs) use ($file, $delimiter) { foreach ($jobs as $job) { $payload = json_decode($job->payload); fputcsv($file, [ $job->id, 'Pending Job', 'PENDING', $job->queue, Carbon::createFromTimestamp($job->available_at)->toDateTimeString(), $payload->displayName ?? 'Unknown', ], $delimiter); } }); // Failed Jobs DB::table('failed_jobs')->orderBy('failed_at', 'desc')->chunk(100, function ($jobs) use ($file, $delimiter) { foreach ($jobs as $job) { fputcsv($file, [ $job->id, 'Failed Job', 'FAILED', $job->queue, $job->failed_at, $job->exception, ], $delimiter); } }); fclose($file); }, $filename, [ 'Content-Type' => $format === 'excel' ? 'application/vnd.ms-excel' : 'text/csv', ]); } /** * TOGGLE MAINTENANCE MODE */ public function toggleMaintenance(Request $request) { $status = $request->get('status'); // true = down, false = up try { if ($status) { Artisan::call('down', [ '--refresh' => 15, '--secret' => 'debug-access', ]); $msg = 'System is now in MAINTENANCE mode.'; } else { Artisan::call('up'); $msg = 'System is now LIVE.'; } return response()->json([ 'success' => true, 'message' => $msg, 'is_down' => app()->isDownForMaintenance(), ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to toggle maintenance mode: '.$e->getMessage(), ], 500); } } /** * AI SECURITY AUDIT */ public function securityAudit(Request $request, SecurityHardeningService $service) { if (! $request->user()->can('view ai log analysis')) { return response()->json(['error' => 'Unauthorized'], 403); } if ($request->has('refresh')) { Cache::forget('security_audit_result'); } $result = $service->auditSecurity(); return response()->json($result); } }