diff --git a/.env b/.env index db17f1a..f80e47e 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -VITE_API_URL='https://wn.krbl.ru' -VITE_REACT_APP ='https://wn.krbl.ru' -VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' +VITE_API_URL='https://wn.st.unprism.ru' +VITE_REACT_APP ='https://wn.st.unprism.ru/' +VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' VITE_NEED_AUTH='true' \ No newline at end of file diff --git a/package.json b/package.json index 43748f2..9a29774 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "white-nights", "private": true, - "version": "1.0.0", + "version": "1.0.1", "type": "module", "license": "UNLICENSED", "scripts": { diff --git a/src/widgets/DevicesTable/DeviceLogsModal.tsx b/src/widgets/DevicesTable/DeviceLogsModal.tsx index eee886c..02610da 100644 --- a/src/widgets/DevicesTable/DeviceLogsModal.tsx +++ b/src/widgets/DevicesTable/DeviceLogsModal.tsx @@ -1,6 +1,6 @@ import { API_URL, authInstance, Modal } from "@shared"; -import { CircularProgress } from "@mui/material"; -import { useEffect, useState } from "react"; +import { CircularProgress, TextField } from "@mui/material"; +import { useEffect, useState, useMemo } from "react"; import { toast } from "react-toastify"; interface DeviceLogChunk { @@ -14,27 +14,110 @@ interface DeviceLogsModalProps { onClose: () => void; } -const formatDate = (date: Date) => { - return new Intl.DateTimeFormat("ru-RU", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }).format(date); +const toYYYYMMDD = (d: Date) => d.toISOString().slice(0, 10); + +type LogLevel = "info" | "warn" | "error" | "debug" | "fatal" | "unknown"; + +const LOG_LEVEL_STYLES: Record = { + info: { + badge: "bg-blue-100 text-blue-700", + text: "text-[#000000BF]", + }, + debug: { + badge: "bg-gray-100 text-gray-600", + text: "text-gray-600", + }, + warn: { + badge: "bg-amber-100 text-amber-700", + text: "text-amber-800", + }, + error: { + badge: "bg-red-100 text-red-700", + text: "text-red-700", + }, + fatal: { + badge: "bg-red-200 text-red-900", + text: "text-red-900 font-semibold", + }, + unknown: { + badge: "bg-gray-100 text-gray-500", + text: "text-[#000000BF]", + }, }; -const formatLogTimestamp = (timestampStr: string) => { - return timestampStr.replace(/^\[|\]$/g, "").trim(); +const formatTs = (raw: string): string => { + try { + const d = new Date(raw); + if (Number.isNaN(d.getTime())) return raw; + const pad = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; + } catch { + return raw; + } +}; + +const parseJsonLogLine = (line: string) => { + try { + const obj = JSON.parse(line); + if (obj && typeof obj === "object") { + const level: LogLevel = + obj.level && obj.level in LOG_LEVEL_STYLES + ? (obj.level as LogLevel) + : "unknown"; + const ts: string = obj.ts ? formatTs(obj.ts) : ""; + const msg: string = obj.msg ?? ""; + + const extra: Record = { ...obj }; + delete extra.level; + delete extra.ts; + delete extra.msg; + delete extra.caller; + const extraStr = Object.keys(extra).length + ? " " + + Object.entries(extra) + .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`) + .join(" ") + : ""; + + return { ts, level, msg, extraStr }; + } + } catch { + } + return null; }; const parseLogLine = (line: string, index: number) => { - const match = line.match(/^(\[[^\]]+\])\s*(.*)$/); - if (match) { - return { id: index, time: match[1], text: match[2].trim() || line }; + const json = parseJsonLogLine(line); + if (json) { + return { + id: index, + time: json.ts, + text: json.msg + json.extraStr, + level: json.level, + sortKey: json.ts, + }; } - return { id: index, time: "", text: line }; -}; -const toYYYYMMDD = (d: Date) => d.toISOString().slice(0, 10); + const bracketMatch = line.match(/^(\[[^\]]+\])\s*(.*)$/); + if (bracketMatch) { + const rawTime = bracketMatch[1].replace(/^\[|\]$/g, "").trim(); + return { + id: index, + time: formatTs(rawTime), + text: bracketMatch[2].trim() || line, + level: "unknown" as LogLevel, + sortKey: rawTime, + }; + } + + return { + id: index, + time: "", + text: line, + level: "unknown" as LogLevel, + sortKey: "", + }; +}; export const DeviceLogsModal = ({ open, @@ -44,10 +127,11 @@ export const DeviceLogsModal = ({ const [chunks, setChunks] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const today = new Date(); const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); - const dateStr = toYYYYMMDD(today); - const dateStrYesterday = toYYYYMMDD(yesterday); + const [dateFrom, setDateFrom] = useState(toYYYYMMDD(yesterday)); + const [dateTo, setDateTo] = useState(toYYYYMMDD(today)); useEffect(() => { if (!open || !deviceUuid) return; @@ -58,7 +142,12 @@ export const DeviceLogsModal = ({ try { const { data } = await authInstance.get( `${API_URL}/devices/${deviceUuid}/logs`, - { params: { from: dateStrYesterday, to: dateStr } } + { + params: { + from: dateFrom, + to: toYYYYMMDD(new Date(new Date(dateTo).getTime() + 86400000)), + }, + } ); setChunks(Array.isArray(data) ? data : []); } catch (err: unknown) { @@ -74,25 +163,51 @@ export const DeviceLogsModal = ({ }; fetchLogs(); - }, [open, deviceUuid, dateStr]); + }, [open, deviceUuid, dateFrom, dateTo]); - const logs = chunks.flatMap((chunk, chunkIdx) => - (chunk.lines ?? []).map((line, i) => - parseLogLine(line, chunkIdx * 10000 + i) - ) - ); + const logs = useMemo(() => { + const parsed = chunks.flatMap((chunk, chunkIdx) => + (chunk.lines ?? []).map((line, i) => + parseLogLine(line, chunkIdx * 10000 + i) + ) + ); + parsed.sort((a, b) => { + if (!a.sortKey && !b.sortKey) return 0; + if (!a.sortKey) return 1; + if (!b.sortKey) return -1; + return b.sortKey.localeCompare(a.sortKey); + }); + return parsed; + }, [chunks]); return ( - +
-
+

Логи

- {formatDate(today)} +
+ setDateFrom(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + setDateTo(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> +
{isLoading && ( -
+
)} @@ -104,21 +219,30 @@ export const DeviceLogsModal = ({ )} {!isLoading && !error && ( -
-
+
+
{logs.length > 0 ? ( - logs.map((log) => ( -
-
- {log.time ? `[${formatLogTimestamp(log.time)}]` : null} + logs.map((log) => { + const style = LOG_LEVEL_STYLES[log.level]; + return ( +
+ + {log.level === "unknown" ? "LOG" : log.level} + + + {log.time || null} + + {log.text}
-
- {log.text} -
-
- )) + ); + }) ) : ( -
+
Логи отсутствуют.
)}