feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,699 @@
|
||||
<?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
|
||||
* ============================================================
|
||||
*
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file is strictly prohibited without prior written
|
||||
* permission from the author.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\SystemSettings;
|
||||
|
||||
use App\Services\AI\SecurityHardeningService;
|
||||
use App\Services\Monitoring\SystemMonitoringService;
|
||||
use App\Support\DataTable;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
|
||||
class SystemMonitoringController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected SystemMonitoringService $monitor
|
||||
) {
|
||||
// Middleware handled in web.php
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
if (! auth()->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 [
|
||||
'<div class="text-nowrap">'.e($log['timestamp']).'</div>',
|
||||
'<span class="badge px-2 py-1 '.$levelClass.'">'.strtoupper(e($log['level'])).'</span>',
|
||||
'<div class="text-truncate small" style="max-width: 600px;" title="'.e($log['message']).'">'.e(str_replace("\n", ' ', substr($log['message'], 0, 300))).'</div>',
|
||||
'<div class="text-end">
|
||||
<button class="btn btn-sm btn-outline-theme view-log-detail" data-log=\''.htmlspecialchars(json_encode($log), ENT_QUOTES, 'UTF-8').'\'>
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>',
|
||||
];
|
||||
}, $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 [
|
||||
'<div class="text-nowrap">'.e($log['timestamp']).'</div>',
|
||||
'<div class="'.$summaryClass.' small">'.e($log['summary']).'</div>',
|
||||
'<div class="text-end">
|
||||
<button class="btn btn-sm btn-outline-theme view-sap-log-detail" data-message=\''.htmlspecialchars($log['message'], ENT_QUOTES, 'UTF-8').'\'>
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>',
|
||||
];
|
||||
}, $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 [
|
||||
'<div class="text-nowrap">'.e($log['timestamp']).'</div>',
|
||||
'<span class="badge px-2 py-1 '.$levelClass.'">'.strtoupper(e($log['level'])).'</span>',
|
||||
'<div class="small fw-bold text-theme-1">'.e($userInfo).'</div>',
|
||||
'<div class="text-truncate small" style="max-width: 500px;">'.e(str_replace("\n", ' ', substr($log['message'], 0, 300))).'</div>',
|
||||
'<div class="text-end">
|
||||
<button class="btn btn-sm btn-outline-theme view-log-detail" data-log=\''.htmlspecialchars(json_encode($log), ENT_QUOTES, 'UTF-8').'\'>
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>',
|
||||
];
|
||||
}, $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'
|
||||
? '<span class="badge px-2 py-1 text-bg-warning">PENDING</span>'
|
||||
: '<span class="badge px-2 py-1 text-bg-danger">FAILED</span>';
|
||||
|
||||
$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 = '<div class="text-truncate small" style="max-width: 400px;" title="'.e($job->exception).'">
|
||||
<strong class="text-danger">'.e($exceptionClass).'</strong>: '.e(str_replace("\n", ' ', $exceptionShort)).'...
|
||||
</div>';
|
||||
} else {
|
||||
$details = '<div class="text-truncate small" style="max-width: 400px;" title="'.e($displayName).'">
|
||||
<strong class="text-dark">'.e($displayName).'</strong>
|
||||
</div>';
|
||||
}
|
||||
|
||||
// 🚀 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 = [
|
||||
'<div class="text-nowrap">'.e($job->timestamp).'</div>',
|
||||
$statusBadge,
|
||||
'<span class="badge text-bg-secondary">'.strtoupper(e($job->queue)).'</span>',
|
||||
$details,
|
||||
'<div class="small fw-bold text-theme-1">'.$taskDescription.'</div>',
|
||||
];
|
||||
|
||||
if ($canManage) {
|
||||
$actions = '<div class="text-end d-flex justify-content-end gap-1">';
|
||||
if ($job->status === 'FAILED') {
|
||||
$actions .= '<button class="btn btn-sm btn-outline-theme retry-job-btn" data-id="'.$job->id.'" title="Retry Job"><i class="bi bi-arrow-repeat"></i></button>';
|
||||
$actions .= '<button class="btn btn-sm btn-outline-theme view-job-detail" data-job=\''.htmlspecialchars(json_encode($job), ENT_QUOTES, 'UTF-8').'\' title="View Detail"><i class="bi bi-eye"></i></button>';
|
||||
$actions .= '<button class="btn btn-sm btn-outline-danger delete-failed-job-btn" data-id="'.$job->id.'" title="Delete Failed Job"><i class="bi bi-trash"></i></button>';
|
||||
} else {
|
||||
// Pending jobs cannot be deleted individually as per user request
|
||||
$actions .= '<button class="btn btn-sm btn-outline-secondary" disabled title="Pending job"><i class="bi bi-hourglass-split"></i></button>';
|
||||
}
|
||||
$actions .= '</div>';
|
||||
$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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user