diff --git a/app/Http/Controllers/Admin/System/LogController.php b/app/Http/Controllers/Admin/System/LogController.php new file mode 100644 index 0000000..d50c6ec --- /dev/null +++ b/app/Http/Controllers/Admin/System/LogController.php @@ -0,0 +1,90 @@ +getLogFiles(); + + // Default to the latest log file if not specified or doesn't exist + $selectedFile = $request->input('file'); + + if (! $selectedFile || ! in_array($selectedFile, $logFiles)) { + $selectedFile = $logFiles[0] ?? null; + } + + $logs = $selectedFile ? $this->parseLogFile($selectedFile) : []; + + return Inertia::render('admin/system/logs/index', [ + 'logFiles' => $logFiles, + 'selectedFile' => $selectedFile, + 'logs' => $logs, + ]); + } + + /** + * Get all .log files from storage/logs. + */ + private function getLogFiles(): array + { + $path = storage_path('logs'); + if (! File::exists($path)) { + return []; + } + + $files = File::files($path); + + return collect($files) + ->map(fn ($file) => $file->getFilename()) + ->filter(fn ($filename) => str_ends_with($filename, '.log')) + ->sortDesc() // Latest dates first + ->values() + ->toArray(); + } + + /** + * Parse a log file into an array of entries. + */ + private function parseLogFile($filename): array + { + $path = storage_path("logs/{$filename}"); + if (! File::exists($path)) { + return []; + } + + // Limit file reading to prevent memory issues with huge logs + // For simplicity, we read the whole file here, but in production consider chunking. + $content = File::get($path); + + // Pattern: [timestamp] environment.LEVEL: Message + // Handles multi-line messages (like stack traces) using lookahead + $pattern = '/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\.(\w+): ([\s\S]*?)(?=\n^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]|$)/m'; + + preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); + + $logs = []; + foreach ($matches as $index => $match) { + $logs[] = [ + 'id' => $index, + 'timestamp' => $match[1], + 'env' => $match[2], + 'level' => strtoupper($match[3]), + 'message' => trim($match[4]), + ]; + } + + // Return latest logs first + return array_reverse($logs); + } +} diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 03da84e..a452982 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -1,5 +1,5 @@ import { Link } from '@inertiajs/react'; -import { Boxes, Currency, DollarSign, LayoutGrid, List, User, Wallet, WalletCardsIcon } from 'lucide-react'; +import { Boxes, Currency, DollarSign, LayoutGrid, List, ScrollText, User, Wallet, WalletCardsIcon } from 'lucide-react'; import AppLogo from '@/components/app-logo'; import { NavMain } from '@/components/nav-main'; import { @@ -18,6 +18,7 @@ import product from '@/routes/product'; import expense from '@/routes/expense'; import payroll from '@/routes/payroll'; import user from '@/routes/user'; +import system from '@/routes/system'; const mainNavItems: NavItem[] = [ { @@ -58,6 +59,14 @@ const financeNavItems: NavItem[] = [ }, ]; +const systemNavItems: NavItem[] = [ + { + title: 'Logs', + href: system.logs.index().url, + icon: ScrollText, + }, +]; + export function AppSidebar() { return ( @@ -77,6 +86,7 @@ export function AppSidebar() { + ); diff --git a/resources/js/pages/admin/system/logs/index.tsx b/resources/js/pages/admin/system/logs/index.tsx new file mode 100644 index 0000000..aa2bd66 --- /dev/null +++ b/resources/js/pages/admin/system/logs/index.tsx @@ -0,0 +1,97 @@ +import { Head, router } from '@inertiajs/react'; +import { Card, CardContent } from '@/components/ui/card'; +import { DataTable } from '@/components/data-table'; +import { useState } from 'react'; +import { getColumns, SystemLog } from './partials/columns'; +import system from '@/routes/system'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { LogDetailModal } from './partials/log-detail-modal'; + +interface Props { + logFiles: string[]; + selectedFile: string; + logs: SystemLog[]; +} + +export default function SystemLogIndex({ logFiles, selectedFile, logs }: Props) { + const [viewingLog, setViewingLog] = useState(null); + + const columns = getColumns({ + onView: (log) => setViewingLog(log) + }); + + const onFileChange = (file: string) => { + router.get(system.logs.index().url, { file }, { + preserveState: true, + preserveScroll: true, + only: ['logs', 'selectedFile'] + }); + }; + + return ( + +
+ + +
+
+

Logs

+
+ +
+ File Log: + +
+
+ + + + + + + + setViewingLog(null)} + /> +
+ ); +} + +SystemLogIndex.layout = { + breadcrumbs: [ + { title: 'Sistem' }, + ], +}; diff --git a/resources/js/pages/admin/system/logs/partials/columns.tsx b/resources/js/pages/admin/system/logs/partials/columns.tsx new file mode 100644 index 0000000..ee749ad --- /dev/null +++ b/resources/js/pages/admin/system/logs/partials/columns.tsx @@ -0,0 +1,104 @@ +import { ColumnDef } from '@tanstack/react-table'; +import { DataTableColumnHeader } from '@/components/data-table-column-header'; +import { Badge } from '@/components/ui/badge'; +import { Eye } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; + +export interface SystemLog { + id: number; + timestamp: string; + env: string; + level: string; + message: string; +} + +interface ColumnProps { + onView: (log: SystemLog) => void; +} + +const getLevelColor = (level: string) => { + switch (level.toUpperCase()) { + case 'EMERGENCY': + case 'ALERT': + case 'CRITICAL': + case 'ERROR': + return 'destructive'; + case 'WARNING': + return 'outline'; // Fallback if no warning variant + case 'NOTICE': + case 'INFO': + return 'secondary'; + case 'DEBUG': + return 'outline'; + default: + return 'default'; + } +} + +export const getColumns = ({ onView }: ColumnProps): ColumnDef[] => [ + { + accessorKey: "timestamp", + header: ({ column }) => ( + + ), + cell: ({ row }) => {row.original.timestamp}, + meta: { title: "Waktu" }, + }, + { + accessorKey: "level", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const level = row.original.level; + return ( + + {level} + + ); + }, + meta: { title: "Level" }, + }, + { + accessorKey: "message", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const message = row.original.message; + return ( +
+ {message} +
+ ); + }, + meta: { title: "Pesan" }, + }, + { + id: "actions", + header: "Aksi", + cell: ({ row }) => { + const log = row.original; + return ( +
+ + + + + +

Detail

+
+
+
+ ); + }, + meta: { title: "Aksi" }, + }, +]; diff --git a/resources/js/pages/admin/system/logs/partials/log-detail-modal.tsx b/resources/js/pages/admin/system/logs/partials/log-detail-modal.tsx new file mode 100644 index 0000000..fe2c443 --- /dev/null +++ b/resources/js/pages/admin/system/logs/partials/log-detail-modal.tsx @@ -0,0 +1,37 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { SystemLog } from "./columns" + +interface Props { + log: SystemLog | null; + onClose: () => void; +} + +export function LogDetailModal({ log, onClose }: Props) { + if (!log) return null; + + return ( + + + +
+ Detail Log + {log.level} + {log.timestamp} +
+
+ +
+
+                        {log.message}
+                    
+
+
+
+ ); +} diff --git a/routes/system.php b/routes/system.php new file mode 100644 index 0000000..13ba14e --- /dev/null +++ b/routes/system.php @@ -0,0 +1,10 @@ +group(function () { + Route::prefix('admin/system')->group(function () { + Route::get('logs', [LogController::class, 'index'])->name('system.logs.index'); + }); +}); diff --git a/routes/web.php b/routes/web.php index b57947d..3b105ea 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,3 +14,4 @@ require __DIR__.'/settings.php'; require __DIR__.'/master.php'; require __DIR__.'/finance.php'; +require __DIR__.'/system.php';