Fixed untracked files
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"WillLuke.nextjs.addTypesOnSave": true,
|
|
||||||
"WillLuke.nextjs.hasPrompted": true,
|
|
||||||
"prismaERDPreviewer.preferredTheme": "dark"
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/* app/admin/logs/logs.module.css */
|
|
||||||
.container {
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterContainer {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logsContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logItem {
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorLog {
|
|
||||||
background-color: #fef2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warnLog {
|
|
||||||
background-color: #fefce8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoLog {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorLevel {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warnLevel {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoLevel {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 24px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #dc2626;
|
|
||||||
text-align: center;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover effects */
|
|
||||||
.logItem:hover {
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select:hover {
|
|
||||||
border-color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus states */
|
|
||||||
.select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #2563eb;
|
|
||||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logHeader {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// app/admin/logs/page.tsx
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import styles from "./logs.module.css";
|
|
||||||
|
|
||||||
interface LogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
level: string;
|
|
||||||
message: string;
|
|
||||||
metadata?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LogsPage() {
|
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [filter, setFilter] = useState<"all" | "error" | "info" | "warn">(
|
|
||||||
"all"
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchLogs();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function fetchLogs() {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/logs/view");
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch logs");
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setLogs(data.logs);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Error fetching logs");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredLogs = logs.filter((log) =>
|
|
||||||
filter === "all" ? true : log.level === filter
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) return <div className={styles.loading}>Loading logs...</div>;
|
|
||||||
if (error) return <div className={styles.error}>Error: {error}</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<h1 className={styles.title}>System Logs</h1>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<select
|
|
||||||
value={filter}
|
|
||||||
onChange={(e) => setFilter(e.target.value as any)}
|
|
||||||
className={styles.select}
|
|
||||||
>
|
|
||||||
<option value="all">All Logs</option>
|
|
||||||
<option value="error">Errors</option>
|
|
||||||
<option value="warn">Warnings</option>
|
|
||||||
<option value="info">Info</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.logsContainer}>
|
|
||||||
{filteredLogs.map((log, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`${styles.logItem} ${
|
|
||||||
log.level === "error"
|
|
||||||
? styles.errorLog
|
|
||||||
: log.level === "warn"
|
|
||||||
? styles.warnLog
|
|
||||||
: styles.infoLog
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={styles.logHeader}>
|
|
||||||
<span className={styles.timestamp}>
|
|
||||||
{format(new Date(log.timestamp), "yyyy-MM-dd HH:mm:ss")}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`${styles.level} ${
|
|
||||||
log.level === "error"
|
|
||||||
? styles.errorLevel
|
|
||||||
: log.level === "warn"
|
|
||||||
? styles.warnLevel
|
|
||||||
: styles.infoLevel
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{log.level.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.message}>{log.message}</div>
|
|
||||||
|
|
||||||
{log.metadata && (
|
|
||||||
<pre className={styles.metadata}>
|
|
||||||
{JSON.stringify(log.metadata, null, 2)}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// app/api/logs/view/route.ts
|
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
interface LogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
level: string;
|
|
||||||
message: string;
|
|
||||||
metadata?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readLogFiles(directory: string): Promise<LogEntry[]> {
|
|
||||||
try {
|
|
||||||
const logPath = path.join(process.cwd(), directory);
|
|
||||||
const files = await fs.readdir(logPath);
|
|
||||||
const logFiles = files.filter((file) => file.endsWith(".log"));
|
|
||||||
|
|
||||||
const allLogs: LogEntry[] = [];
|
|
||||||
|
|
||||||
for (const file of logFiles) {
|
|
||||||
const filePath = path.join(logPath, file);
|
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
|
||||||
|
|
||||||
// Parse setiap baris log
|
|
||||||
const logs = content
|
|
||||||
.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((line) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(line);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
allLogs.push(...logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort berdasarkan timestamp, terbaru di atas
|
|
||||||
return allLogs.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading log files:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Baca logs dari frontend dan backend
|
|
||||||
const frontendLogs = await readLogFiles("logs/frontend");
|
|
||||||
const backendLogs = await readLogFiles("logs/backend");
|
|
||||||
|
|
||||||
// Gabungkan dan sort semua logs
|
|
||||||
const allLogs = [...frontendLogs, ...backendLogs].sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json({ logs: allLogs });
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Failed to fetch logs" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user