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,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] ?? '');
}
}
@@ -0,0 +1,256 @@
<?php
namespace App\Http\Controllers\SystemSettings;
use App\Http\Controllers\Controller;
use App\Services\System\BackupManagementService;
use App\Services\SystemConfig\SystemConfigService;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Laravel\Socialite\Facades\Socialite;
class BackupRestoreController extends Controller
{
protected $backupService;
protected $systemConfig;
public function __construct(
BackupManagementService $backupService,
SystemConfigService $systemConfig
) {
$this->backupService = $backupService;
$this->systemConfig = $systemConfig;
}
public function index(Request $request)
{
if ($request->wantsJson() || $request->ajax()) {
try {
session_write_close();
$driver = $request->get('driver');
return response()->json([
'success' => true,
'backups' => $this->backupService->getBackupList(),
'stats' => $this->backupService->getStorageStats($driver),
]);
} catch (Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
return view('pages.system_settings.backup-restore', [
'settings' => $this->systemConfig->all(),
]);
}
public function create()
{
try {
// Prevent session locking during the long-running backup process
session_write_close();
set_time_limit(0);
ini_set('memory_limit', '512M');
$this->backupService->createBackup();
// ✍️ Log "Action Log" backup creation
activity('backups')
->causedBy(auth()->user())
->withProperties([
'ip' => request()->ip(),
'agent' => request()->userAgent(),
'details' => __('Manually triggered a system and database backup.'),
])
->log(__('Generated System Backup'));
return response()->json([
'success' => true,
'message' => __('Backup created successfully'),
'backups' => $this->backupService->getBackupList(forceRefresh: true),
]);
} catch (Exception $e) {
Log::error('Backup failed: '.$e->getMessage());
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
private function allowedDisks(): array
{
return ['local', 's3', 'gdrive'];
}
public function download(Request $request)
{
$validated = $request->validate([
'disk' => ['required', 'string', Rule::in($this->allowedDisks())],
'path' => ['required', 'string'],
]);
$disk = $validated['disk'];
$path = $validated['path'];
if (! Storage::disk($disk)->exists($path)) {
abort(404, __('File not found'));
}
// ✍️ Log "Action Log" backup download
activity('backups')
->causedBy(auth()->user())
->withProperties([
'ip' => $request->ip(),
'agent' => $request->userAgent(),
'file' => $path,
'disk' => $disk,
'details' => __('Downloaded backup file: :file from :disk storage.', ['file' => $path, 'disk' => $disk]),
])
->log(__('Downloaded Backup File'));
return Storage::disk($disk)->download($path);
}
public function destroy(Request $request)
{
try {
session_write_close();
$validated = $request->validate([
'disk' => ['required', 'string', Rule::in($this->allowedDisks())],
'path' => ['required', 'string'],
]);
$disk = $validated['disk'];
$path = $validated['path'];
if (Storage::disk($disk)->exists($path)) {
Storage::disk($disk)->delete($path);
// ✍️ Log "Action Log" backup deletion
activity('backups')
->causedBy(auth()->user())
->withProperties([
'ip' => $request->ip(),
'agent' => $request->userAgent(),
'file' => $path,
'details' => __('Permanently deleted backup file: :file', ['file' => $path]),
])
->log(__('Deleted Backup File'));
}
return response()->json([
'success' => true,
'message' => __('Backup deleted successfully'),
'backups' => $this->backupService->getBackupList(forceRefresh: true),
]);
} catch (Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
public function restore(Request $request)
{
try {
session_write_close();
set_time_limit(0);
ini_set('memory_limit', '512M');
$validated = $request->validate([
'disk' => ['required', 'string', Rule::in($this->allowedDisks())],
'path' => ['required', 'string'],
]);
$disk = $validated['disk'];
$path = $validated['path'];
$this->backupService->restoreBackup($disk, $path);
// ✍️ Log "Action Log" system restore
activity('backups')
->causedBy(auth()->user())
->withProperties([
'ip' => $request->ip(),
'agent' => $request->userAgent(),
'file' => $path,
'details' => __('Successfully restored system from backup: :file', ['file' => $path]),
])
->log(__('Restored System from Backup'));
return response()->json([
'message' => __('System restored successfully. Cache cleared.'),
]);
} catch (Exception $e) {
Log::error('Restore failed: '.$e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function testConnection()
{
try {
$result = $this->backupService->testConnection();
return response()->json($result);
} catch (Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* Redirect to Google for authorization.
*/
public function googleAuth()
{
$settings = $this->systemConfig->all();
if (empty($settings['gdrive_client_id']) || empty($settings['gdrive_client_secret'])) {
return redirect()->back()->with('error', __('Please save Client ID and Secret first.'));
}
// Dynamically set config for Socialite to use settings from database
config([
'services.google.client_id' => $settings['gdrive_client_id'],
'services.google.client_secret' => $settings['gdrive_client_secret'],
'services.google.redirect' => route('backup-restore.google-callback'),
]);
return Socialite::driver('google')
->scopes(['https://www.googleapis.com/auth/drive.file'])
->with(['access_type' => 'offline', 'prompt' => 'consent'])
->redirect();
}
/**
* Handle Google authorization callback.
*/
public function googleCallback()
{
try {
$settings = $this->systemConfig->all();
config([
'services.google.client_id' => $settings['gdrive_client_id'],
'services.google.client_secret' => $settings['gdrive_client_secret'],
'services.google.redirect' => route('backup-restore.google-callback'),
]);
$user = Socialite::driver('google')->user();
$refreshToken = $user->refreshToken;
if (! $refreshToken) {
return redirect()->route('backup-restore.index')->with('error', __('Failed to get refresh token. Please ensure you have not already authorized this app, or try revoking access and authorizing again.'));
}
// Save the refresh token to system settings
$this->systemConfig->update(['gdrive_refresh_token' => $refreshToken]);
return redirect()->route('backup-restore.index')->with('status', __('Google Drive authorized successfully!'));
} catch (Exception $e) {
Log::error('Google Auth Failed: '.$e->getMessage());
return redirect()->route('backup-restore.index')->with('error', __('Authorization failed: ').$e->getMessage());
}
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\SystemSettings;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class EditorUploadController extends Controller
{
/**
* Handle Image Upload from CKEditor 5 (Simple Upload Adapter)
*/
public function upload(Request $request)
{
if (! $request->hasFile('upload')) {
return response()->json(['error' => ['message' => 'No file uploaded.']], 400);
}
$request->validate([
'upload' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:5120', // 5MB limit
]);
try {
$file = $request->file('upload');
$fileName = $file->getClientOriginalName();
$extension = $file->getClientOriginalExtension();
// Generate clean filename
$name = Str::slug(pathinfo($fileName, PATHINFO_FILENAME));
$finalName = 'editor_'.$name.'_'.time().'.'.$extension;
// Store to public disk (storage/app/public/editor)
$path = $file->storeAs('editor', $finalName, 'public');
return response()->json([
'uploaded' => 1,
'fileName' => $finalName,
'url' => '/storage/'.$path,
]);
} catch (\Exception $e) {
return response()->json(['error' => ['message' => 'Upload failed: '.$e->getMessage()]], 500);
}
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\SystemSettings;
use App\Http\Controllers\Controller;
use App\Services\System\MaintenanceManagementService;
use App\Services\SystemConfig\SystemConfigService;
use Illuminate\Http\Request;
class MaintenanceModeController extends Controller
{
public function __construct(
protected SystemConfigService $systemConfig,
protected MaintenanceManagementService $maintenanceService
) {
// Middleware handled in web.php
}
public function index()
{
return view('pages.system_settings.maintenance-mode', [
'settings' => $this->systemConfig->all(),
'is_down' => $this->maintenanceService->isDown(),
]);
}
public function broadcast(Request $request)
{
$validated = $request->validate([
'minutes' => ['required', 'integer', 'min:1', 'max:60'],
]);
$this->maintenanceService->broadcastWarning($validated['minutes']);
return response()->json([
'success' => true,
'message' => __('Broadcast warning sent to all active users.'),
]);
}
}
@@ -0,0 +1,224 @@
<?php
namespace App\Http\Controllers\SystemSettings;
use App\Events\NotificationSent;
use App\Models\Notification;
use App\Repositories\NotificationRepository;
use App\Services\SystemConfig\SystemConfigService;
use App\Support\DataTable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Models\Role;
class NotificationCenterController extends Controller
{
public function __construct(
protected SystemConfigService $systemConfig,
protected NotificationRepository $notificationRepo
) {
// Middleware handled in web.php
}
protected function ensureFeatureEnabled(): void
{
$featureEnabled = $this->systemConfig->get('feature_notification_center', true);
if (! $featureEnabled) {
// Instead of 404, we change the authorization requirement.
// If feature is disabled, only users who can 'manage global settings' can access.
abort_unless(
Auth::user()?->can('manage global settings'),
403,
__('The Notification Center feature is currently disabled by the system administrator.')
);
}
}
/**
* =====================================================
* INDEX
* =====================================================
*/
public function index(Request $request)
{
$this->ensureFeatureEnabled();
if (DataTable::isDataTableRequest($request)) {
return $this->dataTable($request);
}
$roles = Role::pluck('name')->toArray();
return view('pages.system_settings.notification-center', compact('roles'));
}
/**
* DATATABLE (Supports personal status)
*/
protected function dataTable(Request $request)
{
try {
$user = Auth::user();
$authorizedRecipients = $this->notificationRepo->getAuthorizedRecipients($user);
$start = DataTable::start($request);
$length = DataTable::length($request);
// Efficient query using LEFT JOIN (no N+1)
$baseQuery = Notification::whereIn('recipient', $authorizedRecipients)
->leftJoin('system_notification_user', function ($join) use ($user) {
$join->on('system_notifications.id', '=', 'system_notification_user.notification_id')
->where('system_notification_user.user_id', '=', $user->id);
})
->whereNull('system_notification_user.deleted_at')
->select('system_notifications.*', 'system_notification_user.read_at as personal_read_at');
$recordsTotal = $baseQuery->count();
$recordsFiltered = $recordsTotal;
$notifications = (clone $baseQuery)
->latest('system_notifications.created_at')
->skip($start)
->take($length)
->get();
$rows = $notifications->map(function ($n) {
return [
'id' => $n->id,
'is_unread' => is_null($n->personal_read_at),
'title' => e($n->title),
'message' => e(strip_tags($n->message)),
'type' => $n->type,
'recipient' => $n->recipient,
'time_ago' => $n->created_at ? $n->created_at->diffForHumans() : '',
'read_url' => route('notification-center.read', $n->id),
'delete_url' => route('notification-center.destroy', $n->id),
'created_at' => $n->created_at ? $n->created_at->format('Y-m-d H:i:s') : '',
];
})->values();
return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows->toArray());
} catch (\Exception $e) {
\Log::error('NotificationCenter DataTable Error: '.$e->getMessage());
return response()->json(['error' => $e->getMessage(), 'data' => []], 500);
}
}
/**
* STORE (Send Broadcast)
*/
public function store(Request $request)
{
$this->ensureFeatureEnabled();
$roles = Role::pluck('name')->toArray();
$roles[] = 'all';
$validated = $request->validate([
'title' => 'required|string|max:255',
'message' => 'required|string',
'recipient' => 'required|in:'.implode(',', $roles),
'type' => 'required|in:info,warning,system',
]);
$notification = $this->notificationRepo->create([
...$validated,
'created_by' => Auth::id(),
'read_at' => null,
]);
event(new NotificationSent($notification));
return response()->json(['success' => true, 'message' => __('Notification broadcasted.')]);
}
/**
* MARK AS READ (Personal)
*/
public function markAsRead(Notification $notification)
{
$this->ensureFeatureEnabled();
$this->notificationRepo->markAsRead($notification->id, Auth::id());
return response()->json(['success' => true, 'message' => __('Marked as read.')]);
}
/**
* DELETE (Personal Hide)
*/
public function destroy(Notification $notification)
{
$this->ensureFeatureEnabled();
$this->notificationRepo->personalDelete($notification->id, Auth::id());
return response()->json(['success' => true, 'message' => __('Notification hidden.')]);
}
/**
* MARK ALL AS READ (Personal)
*/
public function markAllAsRead()
{
$this->ensureFeatureEnabled();
$this->notificationRepo->markAllAsReadForUser(Auth::user());
return response()->json(['success' => true, 'message' => __('All marked as read.')]);
}
/**
* CLEAR READ (Personal Hide all read)
*/
public function clearRead()
{
$this->ensureFeatureEnabled();
$user = Auth::user();
$readIds = \DB::table('system_notification_user')
->where('user_id', $user->id)
->whereNotNull('read_at')
->pluck('notification_id');
foreach ($readIds as $id) {
$this->notificationRepo->personalDelete($id, $user->id);
}
return response()->json(['success' => true, 'message' => __('Read notifications cleared from your view.')]);
}
/**
* RECENT NOTIFICATIONS API (Personal status)
*/
public function recentNotifications(Request $request)
{
$this->ensureFeatureEnabled();
$user = Auth::user();
$offset = $request->get('offset', 0);
$limit = $request->get('limit', 10);
$notifications = $this->notificationRepo->getActiveNotificationsForUser($user, $offset, $limit);
$mapped = $notifications->map(function ($n) {
return [
'id' => $n->id,
'title' => e($n->title),
'message' => e(strip_tags($n->message)),
'type' => $n->type,
'time_ago' => $n->created_at->diffForHumans(),
'read_url' => route('notification-center.read', $n->id),
'delete_url' => route('notification-center.destroy', $n->id),
'is_unread' => is_null($n->personal_read_at),
'recipient' => $n->recipient,
];
})->values()->all();
return response()->json([
'success' => true,
'unread_count' => $this->notificationRepo->getUnreadCount($user),
'notifications' => $mapped,
'has_more' => count($mapped) === (int) $limit,
]);
}
}
@@ -0,0 +1,310 @@
<?php
namespace App\Http\Controllers\SystemSettings;
use App\Helpers\SessionHelper;
use App\Services\Monitoring\SystemMonitoringService;
use App\Support\DataTable;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
class SessionManagerController extends Controller
{
public function __construct()
{
// Middleware handled in web.php
}
public function index(Request $request)
{
if (DataTable::isDataTableRequest($request)) {
return $this->dataTable($request);
}
$stats = $this->getStatsData();
return view('pages.system_settings.session-manager', compact('stats'));
}
protected function getDriver()
{
return config('session.driver');
}
public function getStats()
{
return response()->json($this->getStatsData());
}
protected function getStatsData()
{
$driver = $this->getDriver();
if ($driver === 'database') {
$activeCutoff = now()->subMinutes(30)->timestamp;
return [
'total' => DB::table('sessions')->count(),
'active' => DB::table('sessions')->where('last_activity', '>=', $activeCutoff)->count(),
'users' => DB::table('sessions')->whereNotNull('user_id')->count(),
'guests' => DB::table('sessions')->whereNull('user_id')->count(),
'unique_ips' => DB::table('sessions')->distinct('ip_address')->count('ip_address'),
];
}
if ($driver === 'redis') {
$service = app(SystemMonitoringService::class);
$total = $service->getActiveUsers();
// For Redis, we'd need to iterate all to get specific user/guest breakdown
// For performance, we'll return total for now or a limited set
return [
'total' => $total,
'active' => $total,
'users' => 'N/A (Redis)',
'guests' => 'N/A (Redis)',
'unique_ips' => 'N/A (Redis)',
];
}
return [
'total' => 0, 'active' => 0, 'users' => 0, 'guests' => 0, 'unique_ips' => 0,
];
}
protected function dataTable(Request $request)
{
$driver = $this->getDriver();
$sessions = [];
$recordsTotal = 0;
$recordsFiltered = 0;
$activeCutoff = now()->subMinutes(30)->timestamp;
$idleCutoff = now()->subMinutes(5)->timestamp;
$currentSessionId = session()->getId();
if ($driver === 'database') {
$query = DB::table('sessions')
->leftJoin('users', 'sessions.user_id', '=', 'users.id')
->select(
'sessions.*',
'users.email as user_email',
'users.name as user_name'
);
$recordsTotal = DB::table('sessions')->count();
// Status Filter
if ($status = DataTable::columnSearch($request, 0)) {
$operator = $status === 'active' ? '>=' : '<';
$query->where('sessions.last_activity', $operator, $activeCutoff);
}
// User Filter
if ($userSearch = DataTable::columnSearch($request, 1)) {
$query->where(function ($q) use ($userSearch) {
$q->where('users.email', 'like', "%{$userSearch}%")
->orWhere('users.name', 'like', "%{$userSearch}%");
});
}
// IP Filter
if ($ipSearch = DataTable::columnSearch($request, 3)) {
$query->where('sessions.ip_address', 'like', "%{$ipSearch}%");
}
// Global Search
if ($globalSearch = DataTable::globalSearch($request)) {
$query->where(function ($q) use ($globalSearch) {
$q->where('users.email', 'like', "%{$globalSearch}%")
->orWhere('users.name', 'like', "%{$globalSearch}%")
->orWhere('sessions.id', 'like', "%{$globalSearch}%")
->orWhere('sessions.ip_address', 'like', "%{$globalSearch}%");
});
}
$recordsFiltered = (clone $query)->count();
[$orderIndex, $orderDirection] = DataTable::order($request, 4, 'desc');
$sortColumn = match ($orderIndex) {
0 => 'sessions.last_activity',
1 => 'users.name',
3 => 'sessions.ip_address',
4 => 'sessions.last_activity',
default => 'sessions.last_activity',
};
$sessions = $query
->orderBy($sortColumn, $orderDirection)
->skip(DataTable::start($request))
->take(DataTable::length($request))
->get();
} else {
// REDIS DRIVER LOGIC
$connection = config('session.connection') ?? 'default';
$redis = Redis::connection($connection);
$sessionCookie = config('session.cookie', 'laravel_session');
$prefix = config('database.redis.options.prefix', '');
// Optimization: Get keys once
$patterns = [$sessionCookie.':*'];
$keys = [];
foreach ($patterns as $p) {
$searchPattern = str_replace($prefix, '', $p);
$keys = array_merge($keys, $redis->keys($searchPattern));
}
$keys = array_unique($keys);
$recordsTotal = count($keys);
$recordsFiltered = $recordsTotal;
$start = DataTable::start($request);
$length = DataTable::length($request);
$pagedKeys = array_slice($keys, $start, $length);
$tempSessions = [];
$userIds = [];
foreach ($pagedKeys as $key) {
$pureKey = str_replace($prefix, '', $key);
$data = $redis->get($pureKey);
if ($data) {
try {
$unserialized = unserialize($data, ['allowed_classes' => false]);
if (is_string($unserialized)) {
$unserialized = unserialize($unserialized, ['allowed_classes' => false]);
}
} catch (\Exception $e) {
continue;
}
$parts = explode(':', $key);
$sessionId = end($parts);
if (isset($unserialized['user_id'])) {
$userIds[] = $unserialized['user_id'];
}
$tempSessions[] = [
'id' => $sessionId,
'user_id' => $unserialized['user_id'] ?? null,
'ip_address' => $unserialized['ip_address'] ?? 'N/A',
'user_agent' => $unserialized['user_agent'] ?? 'N/A',
'last_activity' => $unserialized['last_activity'] ?? time(),
];
}
}
// Batch fetch users to prevent N+1
$users = ! empty($userIds) ? DB::table('users')->whereIn('id', array_unique($userIds))->get()->keyBy('id') : collect();
foreach ($tempSessions as $sessData) {
$user = $users->get($sessData['user_id']);
$sessions[] = (object) array_merge($sessData, [
'user_name' => $user->name ?? 'Guest',
'user_email' => $user->email ?? 'N/A',
]);
}
}
$rows = collect($sessions)->map(function ($session) use ($activeCutoff, $idleCutoff, $currentSessionId) {
$isCurrent = $session->id === $currentSessionId;
$userAgent = SessionHelper::parseUserAgent($session->user_agent);
// Status Logic
if ($isCurrent) {
$statusHtml = '<span class="badge text-bg-success border border-success px-3 py-2 small fw-bold">
<span class="spinner-grow spinner-grow-sm me-1" role="status"></span>'.__('LIVE').'
</span>';
} elseif ($session->last_activity >= $idleCutoff) {
$statusHtml = '<span class="badge text-bg-info px-3 py-2 small fw-bold">'.__('ACTIVE').'</span>';
} elseif ($session->last_activity >= $activeCutoff) {
$statusHtml = '<span class="badge text-bg-warning px-3 py-2 small fw-bold">'.__('IDLE').'</span>';
} else {
$statusHtml = '<span class="badge text-bg-secondary px-3 py-2 small">'.__('EXPIRED').'</span>';
}
// User Column
$userHtml = '<div class="d-flex align-items-center">
<div>
<div class="fw-bold small">'.e($session->user_name ?? 'Guest').'</div>
<div class="text-secondary extra-small">'.e($session->user_email ?? substr($session->id, 0, 8).'...').'</div>
</div>
</div>';
// Device Column
$deviceHtml = '<div class="d-flex align-items-center">
<i class="bi '.$userAgent['browser_icon'].' h5 mb-0 me-2 text-theme-1"></i>
<div>
<div class="small fw-medium">'.$userAgent['browser'].'</div>
<div class="extra-small text-secondary"><i class="bi '.$userAgent['os_icon'].' me-1"></i>'.$userAgent['os'].'</div>
</div>
</div>';
$timestamp = Carbon::createFromTimestamp($session->last_activity);
$actionsHtml = '<div class="text-end d-flex justify-content-end gap-2">';
$actionsHtml .= '<button class="btn btn-square btn-outline-theme btn-sm rounded-circle btn-detail-session"
data-activity=\''.e(json_encode([
'session' => $session,
'device' => $userAgent,
'is_current' => $isCurrent,
'time' => format_datetime($timestamp),
], JSON_HEX_APOS | JSON_HEX_QUOT)).'\'>
<i class="bi bi-eye"></i>
</button>';
if (! $isCurrent) {
if (auth()->user()->can('manage active sessions')) {
$actionsHtml .= '<button type="button" class="btn btn-square btn-outline-danger btn-sm rounded-circle btn-terminate-session"
data-id="'.e($session->id).'"
data-url="'.route('session-manager.terminate', $session->id).'">
<i class="bi bi-x-lg"></i>
</button>';
} else {
$actionsHtml .= '<button class="btn btn-square btn-light btn-sm rounded-circle" disabled title="Insufficient permission">
<i class="bi bi-shield-lock text-muted"></i>
</button>';
}
} else {
$actionsHtml .= '<button class="btn btn-square btn-light btn-sm rounded-circle" disabled title="Current session cannot be terminated">
<i class="bi bi-lock-fill text-muted"></i>
</button>';
}
$actionsHtml .= '</div>';
return [
$statusHtml,
$userHtml,
$deviceHtml,
'<code class="extra-small">'.e($session->ip_address).'</code>',
e($timestamp->diffForHumans()),
$actionsHtml,
$isCurrent, // For custom row coloring in JS
];
})->all();
return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows);
}
public function destroy(Request $request, $id)
{
if ($id === session()->getId()) {
return response()->json(['success' => false, 'message' => __('Cannot terminate current session.')], 403);
}
$driver = $this->getDriver();
if ($driver === 'database') {
DB::table('sessions')->where('id', $id)->delete();
} elseif ($driver === 'redis') {
$connection = config('session.connection') ?? 'default';
$redis = Redis::connection($connection);
$sessionCookie = config('session.cookie', 'laravel_session');
$redis->del($sessionCookie.':'.$id);
}
return response()->json(['success' => true, 'message' => __('Session terminated successfully.')]);
}
}
@@ -0,0 +1,267 @@
<?php
namespace App\Http\Controllers\SystemSettings;
use App\Http\Requests\SystemSettings\UpdateSystemConfigRequest;
use App\Models\AI\AiUsageLog;
use App\Services\AI\AiService;
use App\Services\System\MaintenanceManagementService;
use App\Services\SystemConfig\SystemConfigService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use SAPNWRFC\Connection;
class SystemConfigController extends Controller
{
public function __construct(
protected SystemConfigService $systemConfig,
protected MaintenanceManagementService $maintenanceService,
protected AiService $aiService
) {
// Middleware handled in web.php
}
public function index()
{
return view('pages.system_settings.system-config', [
'settings' => $this->systemConfig->all(),
'groups' => $this->systemConfig->grouped(),
'is_down' => $this->maintenanceService->isDown(),
'ai_models' => AiService::getSupportedModels(),
]);
}
public function update(UpdateSystemConfigRequest $request)
{
$this->systemConfig->update(
input: $request->validated(),
files: $request->allFiles(),
actorId: $request->user()?->id,
request: $request,
);
// ✍️ Log "Action Log" bulk update
activity('system-config')
->causedBy($request->user())
->withProperties([
'ip' => $request->ip(),
'agent' => $request->userAgent(),
'details' => __('Bulk system configuration update performed.'),
])
->log(__('Updated System Settings'));
// Only sync maintenance state if maintenance fields were part of the request
$maintenanceKeys = [
'maintenance_mode_enabled',
'maintenance_mode_secret',
'maintenance_mode_allowed_ips',
'maintenance_mode_end_at',
];
if ($request->hasAny($maintenanceKeys)) {
$this->maintenanceService->syncState();
}
if ($request->expectsJson()) {
return response()->json([
'success' => true,
'message' => __('Configuration updated successfully!'),
'settings' => $this->systemConfig->all(true),
'is_down' => $this->maintenanceService->isDown(),
]);
}
return back()->with('success', __('Configuration updated successfully!'));
}
public function testEmail(Request $request): JsonResponse
{
$to = $request->input('to') ?: $this->systemConfig->get('mail_from_address');
if (! $to || ! filter_var($to, FILTER_VALIDATE_EMAIL)) {
return response()->json(['success' => false, 'message' => __('No valid recipient email address. Please enter a valid email.')], 422);
}
try {
Mail::raw(
__('This automated message confirms that the SMTP configuration for :app has been successfully verified and is operating correctly.', ['app' => config('app.name')]),
function ($message) use ($to) {
$message->to($to)
->subject(__('SMTP Configuration Verification — :app', ['app' => config('app.name')]));
}
);
// ✍️ Log "Action Log" test email
activity('system-config')
->causedBy($request->user())
->withProperties([
'ip' => $request->ip(),
'agent' => $request->userAgent(),
'recipient' => $to,
'details' => __('Sent a test email to :email to verify SMTP configuration.', ['email' => $to]),
])
->log(__('Sent Test Email'));
return response()->json(['success' => true, 'message' => __('Test email sent successfully to :email', ['email' => $to])]);
} catch (\Exception $e) {
Log::error('Test email failed: '.$e->getMessage());
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
public function testSapConnection(Request $request): JsonResponse
{
$config = [
'ashost' => $request->input('sap_rfc_ashost'),
'sysnr' => $request->input('sap_rfc_sysnr'),
'client' => $request->input('sap_rfc_client'),
'user' => $request->input('sap_rfc_user'),
'passwd' => $request->input('sap_rfc_passwd'),
'router' => $request->input('sap_rfc_router'),
];
// Check if required fields are present
if (empty($config['ashost']) || empty($config['sysnr']) || empty($config['user'])) {
return response()->json(['success' => false, 'message' => __('Host, System Number, and User are required for testing.')], 422);
}
if (! extension_loaded('sapnwrfc')) {
return response()->json([
'success' => false,
'message' => __('The sapnwrfc PHP extension is not installed on this server. Connection cannot be established.'),
], 500);
}
try {
// Attempt to connect using the extension
$conn = new Connection($config);
$attributes = $conn->getAttributes();
$conn->close();
activity('system-config')
->causedBy($request->user())
->withProperties(['host' => $config['ashost']])
->log(__('Successful SAP RFC connection test'));
return response()->json([
'success' => true,
'message' => __('Successfully connected to SAP! (System: :sysid)', ['sysid' => $attributes['sysId'] ?? 'N/A']),
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'SAP Error: '.$e->getMessage()], 500);
}
}
public function testDatabaseConnection(Request $request): JsonResponse
{
try {
\Illuminate\Support\Facades\DB::connection()->getPdo();
$dbName = \Illuminate\Support\Facades\DB::connection()->getDatabaseName();
activity('system-config')
->causedBy($request->user())
->withProperties(['db' => $dbName])
->log(__('Successful database connection test'));
return response()->json([
'success' => true,
'message' => __('Database connection verified successfully! Connected to: :db', ['db' => $dbName]),
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Database Error: '.$e->getMessage()], 500);
}
}
public function publicConfig(): JsonResponse
{
$settings = $this->systemConfig->getPublicSettings();
if (! empty($settings['app_logo'])) {
$settings['app_logo_url'] = asset($settings['app_logo']);
}
if (! empty($settings['app_favicon'])) {
$settings['app_favicon_url'] = asset($settings['app_favicon']);
}
return response()
->json([
'data' => $settings,
'generated_at' => now()->toIso8601String(),
])
->header('Cache-Control', 'public, max-age=300, s-maxage=300');
}
public function simulateAi(Request $request): JsonResponse
{
$prompt = $request->input('prompt');
$providerId = $request->input('ai_provider', $this->systemConfig->get('ai_provider', 'gpt'));
if (! $prompt) {
return response()->json(['success' => false, 'message' => __('Prompt is required.')], 422);
}
try {
$options = [
'key' => $request->input('ai_key'),
'model' => $request->input('ai_model'),
'instruction' => $request->input('ai_instruction'),
'temperature' => $request->input('ai_temperature'),
'max_tokens' => $request->input('ai_max_tokens'),
];
$result = $this->aiService->provider($providerId)->generate($prompt, $options);
if (! $result['success']) {
return response()->json(['success' => false, 'message' => $result['error']], 500);
}
return response()->json([
'success' => true,
'provider' => $providerId,
'response' => $result['response'],
'usage' => $result['usage'] ?? null,
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
public function getAiUsageStats(Request $request): JsonResponse
{
$period = $request->input('period', 'all');
$query = AiUsageLog::query();
if ($period !== 'all') {
$date = match ($period) {
'day' => now()->startOfDay(),
'week' => now()->startOfWeek(),
'month' => now()->startOfMonth(),
'year' => now()->startOfYear(),
default => null
};
if ($date) {
$query->where('created_at', '>=', $date);
}
}
$stats = [
'total_requests' => (clone $query)->count(),
'total_tokens' => (clone $query)->sum('total_tokens'),
'total_cost' => (clone $query)->sum('estimated_cost'),
'providers' => (clone $query)->select('provider', \DB::raw('count(*) as count'))
->groupBy('provider')
->get(),
'recent' => (clone $query)->latest()->take(5)->get(),
'period' => $period,
];
return response()->json(['success' => true, 'stats' => $stats]);
}
}
@@ -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);
}
}