Update controller get list log, lưu cache redis, fix UI

This commit is contained in:
andrew.ng 2026-06-05 08:08:55 +07:00
parent 7ca2503508
commit a69e904afa
6 changed files with 152 additions and 63 deletions

View File

@ -104,4 +104,57 @@ export default class LogsController {
}) })
} }
} }
async listSystemLogFiles({ request, response }: HttpContext) {
try {
const filename = request.input('filename', '')
const fromDate = request.input('from_date', '')
const toDate = request.input('to_date', '')
const systemLogsDir = path.join(DIRNAME, '..', '..', 'storage', 'system_logs')
// Nếu thư mục không tồn tại, trả về danh sách rỗng
if (!fs.existsSync(systemLogsDir)) {
return response.ok({
status: true,
data: [],
})
}
let files = fs.readdirSync(systemLogsDir)
// Lọc theo tên file nếu có
if (filename) {
files = files.filter((f) => f.includes(filename))
}
// Lọc theo khoảng thời gian nếu có
if (fromDate || toDate) {
const fromTime = fromDate ? new Date(fromDate).getTime() : 0
// Cộng thêm 24h để bao gồm cả ngày cuối cùng (end of day 23:59:59)
const toTime = toDate ? new Date(toDate).getTime() + 24 * 60 * 60 * 1000 : Date.now()
files = files.filter((f) => {
const filePath = path.join(systemLogsDir, f)
const stat = fs.statSync(filePath)
const fileTime = stat.mtime.getTime()
return fileTime >= fromTime && fileTime < toTime
})
}
// Lấy 100 tên file đầu tiên từ danh sách lọc được
const result = files.slice(0, 100)
return response.ok({
status: true,
data: result,
})
} catch (error) {
return response.internalServerError({
status: false,
message: 'Failed to list system log files',
error: error.message,
})
}
}
} }

View File

@ -655,6 +655,21 @@ export default class LineConnection {
}, },
data, data,
}) })
this.addHistory(
this.config.stationId,
this.config.id,
{
id: this.config.id,
number: this.config.lineNumber,
stationId: this.config.stationId,
pid: this.config.inventory?.pid || '',
sn: this.config.inventory?.sn || '',
vid: this.config.inventory?.vid || '',
scenario: '',
timestamp: Date.now(),
},
this.outputTestLog,
)
// if (script?.send_result || script?.sendResult) { // if (script?.send_result || script?.sendResult) {
this.dataDPELP = result this.dataDPELP = result
console.log( console.log(

View File

@ -36,6 +36,9 @@ const parseLog = (data: string) => {
// Update current record with matched fields // Update current record with matched fields
Object.keys(currentRecord).forEach((key) => { Object.keys(currentRecord).forEach((key) => {
if (item && item[key] !== undefined) { if (item && item[key] !== undefined) {
if (key === "pid") {
item[key] = item[key].replace("(", '').replace(")", '')
}
currentRecord[key] = item[key].trim() currentRecord[key] = item[key].trim()
} }
}) })

View File

@ -32,6 +32,7 @@ router
router.get('list', '#controllers/logs_controller.list') router.get('list', '#controllers/logs_controller.list')
router.post('viewLog', '#controllers/logs_controller.viewLog') router.post('viewLog', '#controllers/logs_controller.viewLog')
router.post('downloadLog', '#controllers/logs_controller.downloadLog') router.post('downloadLog', '#controllers/logs_controller.downloadLog')
router.get('listSystemLogFiles', '#controllers/logs_controller.listSystemLogFiles')
}) })
.prefix('api/logs') .prefix('api/logs')

View File

@ -12,7 +12,7 @@ import {
Loader, Loader,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DateInput } from "@mantine/dates";
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react";
import type { ISystemLog } from "../../untils/types"; import type { ISystemLog } from "../../untils/types";
import { import {
IconDownload, IconDownload,
@ -24,6 +24,7 @@ import classes from "../Component.module.css";
import moment from "moment"; import moment from "moment";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import ModalLog from "../Modal/ModalLog"; import ModalLog from "../Modal/ModalLog";
import axios from "axios";
function DrawerLogs({ function DrawerLogs({
socket, socket,
@ -41,45 +42,73 @@ function DrawerLogs({
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [systemLogs, setSystemLogs] = useState<ISystemLog[]>([]); const [systemLogs, setSystemLogs] = useState<ISystemLog[]>([]);
const [isDownloadLog, setIsDownloadLog] = useState(false); const [isDownloadLog, setIsDownloadLog] = useState(false);
// const [testLogContent, setTestLogContent] = useState(""); const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [downloadName, setDownloadName] = useState(""); const [downloadName, setDownloadName] = useState("");
const [searchFileName, setSearchFileName] = useState(""); const [searchFileName, setSearchFileName] = useState("");
const [fromDate, setFromDate] = useState<Date | null>(null); const [fromDate, setFromDate] = useState<Date | null>(null);
const [toDate, setToDate] = useState<Date | null>(null); const [toDate, setToDate] = useState<Date | null>(null);
const [filteredLogs, setFilteredLogs] = useState<ISystemLog[]>([]); const [filteredLogs, setFilteredLogs] = useState<ISystemLog[]>([]);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const apiUrl = import.meta.env.VITE_BACKEND_URL;
const fetchSystemLogFiles = async (
filename: string,
fromDateVal: Date | null,
toDateVal: Date | null,
) => {
try {
setLoading(true);
const params: Record<string, string> = {};
if (filename) {
params.filename = filename;
}
if (fromDateVal) {
params.from_date = moment(fromDateVal).format("YYYY-MM-DD");
}
if (toDateVal) {
params.to_date = moment(toDateVal).format("YYYY-MM-DD");
}
const response = await axios.get(`${apiUrl}api/logs/listSystemLogFiles`, {
params,
});
if (response.data && response.data.data) {
const list: ISystemLog[] = response.data.data.map((file: string) => {
const filename = file.replace(/^.*[\\/]/, "");
const createAt = filename.match(/\d{8}/);
return {
fileName:
file.split("/")[3] ||
file.split("/")[2] ||
file.split("/")[1] ||
file.split("/")[0],
createdAt: createAt ? createAt[0] : "N/A",
path: file,
};
});
setSystemLogs(
list.sort(
(a: ISystemLog, b: ISystemLog) =>
parseInt(b.createdAt) - parseInt(a.createdAt),
),
);
}
setLoading(false);
} catch (error) {
console.error("Failed to fetch system log files:", error);
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
if (opened) { if (opened) {
socket?.emit("get_list_logs"); fetchSystemLogFiles("", null, null);
} }
}, [opened]); }, [opened]);
useEffect(() => {
socket?.on("list_logs", (files: string[]) => {
const list: ISystemLog[] = files.map((file) => {
const filename = file.replace(/^.*[\\/]/, "");
const createAt = filename.match(/\d{8}/);
return {
fileName:
file.split("/")[3] || file.split("/")[2] || file.split("/")[1],
createdAt: createAt ? createAt[0] : "N/A",
path: file,
};
});
setTimeout(() => {
setLoading(false);
}, 1000);
setSystemLogs(
list.sort(
(a: ISystemLog, b: ISystemLog) =>
parseInt(b.createdAt) - parseInt(a.createdAt)
)
);
});
}, [socket]);
useEffect(() => { useEffect(() => {
if (isDownloadLog && testLogContent && downloadName) { if (isDownloadLog && testLogContent && downloadName) {
const blob = new Blob([testLogContent], { type: "text/plain" }); const blob = new Blob([testLogContent], { type: "text/plain" });
@ -98,40 +127,27 @@ function DrawerLogs({
}, [testLogContent]); }, [testLogContent]);
useEffect(() => { useEffect(() => {
// Chuẩn bị trước các giá trị search/date để tránh tính lại trong filter cho từng phần tử // Clear previous debounce timer
const trimmedSearch = searchFileName.trim().toLowerCase(); if (debounceTimerRef.current) {
const hasSearch = trimmedSearch.length > 0; clearTimeout(debounceTimerRef.current);
const fromMoment = fromDate ? moment(fromDate).startOf("day") : null; }
const toMoment = toDate ? moment(toDate).endOf("day") : null;
const delayDebounceFn = setTimeout(() => { // Set new debounce timer for API call
// Nếu không có filter nào, tránh filter tốn công, gán thẳng debounceTimerRef.current = setTimeout(() => {
if (!hasSearch && !fromMoment && !toMoment) { fetchSystemLogFiles(searchFileName, fromDate, toDate);
setFilteredLogs(systemLogs);
return;
}
const next = systemLogs.filter((log) => {
if (hasSearch && !log.fileName.toLowerCase().includes(trimmedSearch)) {
return false;
}
const logDate = moment(log.createdAt, "YYYYMMDD");
if (fromMoment && !logDate.isSameOrAfter(fromMoment)) {
return false;
}
if (toMoment && !logDate.isSameOrBefore(toMoment)) {
return false;
}
return true;
});
setFilteredLogs(next);
}, 500); }, 500);
return () => clearTimeout(delayDebounceFn); return () => {
}, [searchFileName, fromDate, toDate, systemLogs]); if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, [searchFileName, fromDate, toDate]);
useEffect(() => {
// API already returns filtered results, so just update filteredLogs
setFilteredLogs(systemLogs);
}, [systemLogs]);
return ( return (
<> <>
@ -289,7 +305,7 @@ function DrawerLogs({
setDownloadName( setDownloadName(
element.path.split("/")[3] || element.path.split("/")[3] ||
element.path.split("/")[2] || element.path.split("/")[2] ||
element.path.split("/")[1] element.path.split("/")[1],
); );
}} }}
width={20} width={20}

View File

@ -888,7 +888,7 @@ const ModalTerminal = ({
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
<Flex direction={"column"} h={"75%"}> <Flex direction={"column"} h={"80vh"}>
<Box> <Box>
<Flex gap={"sm"} justify={"center"} align={"center"}> <Flex gap={"sm"} justify={"center"} align={"center"}>
<Text size="xl"> <Text size="xl">
@ -1087,7 +1087,7 @@ const ModalTerminal = ({
</Flex> </Flex>
</Card> </Card>
</Box> </Box>
<Tabs.Panel value="general" h={"95%"}> <Tabs.Panel value="general" h={"70vh"}>
<Flex <Flex
justify={"space-between"} justify={"space-between"}
direction={"column"} direction={"column"}
@ -1249,6 +1249,7 @@ const ModalTerminal = ({
withBorder withBorder
h={"fit-content"} h={"fit-content"}
mt={"4px"} mt={"4px"}
pb={10}
style={{ style={{
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",