feat: add app and database modules

This commit is contained in:
2026-05-21 16:05:11 +07:00
parent 37b7e783f5
commit fad70d096b
212 changed files with 23901 additions and 0 deletions
@@ -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);
}
}