From a148f56b223e2f0ba39bd4d2e9352becfeda071f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Feb 2026 20:23:04 +0000 Subject: [PATCH 1/6] logs tab --- client/src/Features/UI/uiSlice.js | 8 +- client/src/Pages/Logs/TabDiagnostics.tsx | 3 + client/src/Pages/Logs/TabLogs.tsx | 63 +++++++++ client/src/Pages/Logs/TabQueue.tsx | 3 + client/src/Pages/Logs/TableLogs.tsx | 140 +++++++++++++++++++ client/src/Pages/Logs/index.tsx | 29 ++++ client/src/Pages/Logs/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 6 +- client/src/Types/Log.ts | 13 ++ client/src/Types/state.ts | 3 + client/src/locales/en.json | 26 ++++ client/vite.config.ts | 2 +- 12 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 client/src/Pages/Logs/TabDiagnostics.tsx create mode 100644 client/src/Pages/Logs/TabLogs.tsx create mode 100644 client/src/Pages/Logs/TabQueue.tsx create mode 100644 client/src/Pages/Logs/TableLogs.tsx create mode 100644 client/src/Pages/Logs/index.tsx rename client/src/Pages/Logs/{index.jsx => old.jsx} (100%) create mode 100644 client/src/Types/Log.ts diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 65aff99a8..014940a9a 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -19,6 +19,9 @@ const initialState = { infrastructure: { rowsPerPage: 5, }, + logs: { + rowsPerPage: 15, + }, sidebar: { collapsed: false, }, @@ -41,9 +44,10 @@ const uiSlice = createSlice({ }, setRowsPerPage: (state, action) => { const { table, value } = action.payload; - if (state[table]) { - state[table].rowsPerPage = value; + if (!state[table]) { + state[table] = {}; } + state[table].rowsPerPage = value; }, toggleSidebar: (state) => { state.sidebar.collapsed = !state.sidebar.collapsed; diff --git a/client/src/Pages/Logs/TabDiagnostics.tsx b/client/src/Pages/Logs/TabDiagnostics.tsx new file mode 100644 index 000000000..92b8801e0 --- /dev/null +++ b/client/src/Pages/Logs/TabDiagnostics.tsx @@ -0,0 +1,3 @@ +export const TabDiagnostics = () => { + return "Diagnostics"; +}; diff --git a/client/src/Pages/Logs/TabLogs.tsx b/client/src/Pages/Logs/TabLogs.tsx new file mode 100644 index 000000000..b4d601429 --- /dev/null +++ b/client/src/Pages/Logs/TabLogs.tsx @@ -0,0 +1,63 @@ +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import { useSelector } from "react-redux"; +import { Select } from "@/Components/v2/inputs"; +import { TableLogs } from "./TableLogs"; + +import { useTheme } from "@mui/material"; +import { useGet } from "@/Hooks/UseApi"; +import { useState } from "react"; +import type { Log, LogLevelOption } from "@/Types/Log"; +import { LOG_LEVEL_OPTIONS } from "@/Types/Log"; +import { t } from "i18next"; +import type { RootState } from "@/Types/state"; + +export const TabLogs = () => { + const theme = useTheme(); + const { data: logs } = useGet("/logs"); + const [selectedLogLevel, setSelectedLogLevel] = useState("all"); + const [page, setPage] = useState(0); + const rowsPerPage = useSelector( + (state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15 + ); + + const filteredLogs = logs + ?.filter((log) => { + if (selectedLogLevel === "all") return true; + return log.level === selectedLogLevel; + }) + .reverse() + .map((log, idx) => ({ ...log, id: idx })); + + const paginatedLogs = + filteredLogs?.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) ?? []; + + return ( + + + + + ); +}; diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx new file mode 100644 index 000000000..e327218d3 --- /dev/null +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -0,0 +1,3 @@ +export const TabQueue = () => { + return "Queue"; +}; diff --git a/client/src/Pages/Logs/TableLogs.tsx b/client/src/Pages/Logs/TableLogs.tsx new file mode 100644 index 000000000..e565739bd --- /dev/null +++ b/client/src/Pages/Logs/TableLogs.tsx @@ -0,0 +1,140 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table } from "@/Components/v2/design-elements"; +import { Pagination } from "@/Components/v2/design-elements/Table"; +import { useTheme } from "@mui/material"; +import type { Header } from "@/Components/v2/design-elements/Table"; +import type { Log, LogLevel } from "@/Types/Log"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import type { RootState } from "@/Types/state"; +import { setRowsPerPage } from "@/Features/UI/uiSlice"; + +type LogWithId = Log & { id: number }; + +interface TableLogsProps { + logs: LogWithId[]; + logCount: number; + page: number; + setPage: (page: number) => void; +} + +const LevelBadge = ({ level }: { level: LogLevel }) => { + const theme = useTheme(); + + const levelColors: Record = { + info: theme.palette.success.main, + warn: theme.palette.warning.main, + error: theme.palette.error.main, + debug: theme.palette.info.main, + }; + + const color = levelColors[level] || theme.palette.text.primary; + + return ( + + {level} + + ); +}; + +const formatTimestamp = (timestamp: string): string => { + if (!timestamp) return "-"; + const date = new Date(timestamp); + return date.toLocaleString(); +}; + +export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const rowsPerPage = useSelector( + (state: RootState) => state?.ui?.logs?.rowsPerPage ?? 15 + ); + + const headers: Header[] = [ + { + id: "timestamp", + content: t("pages.logs.table.headers.timestamp"), + render: (row) => ( + + {formatTimestamp(row.timestamp)} + + ), + }, + { + id: "level", + content: t("pages.logs.table.headers.level"), + render: (row) => , + }, + { + id: "service", + content: t("pages.logs.table.headers.service"), + render: (row) => ( + {row.service || "-"} + ), + }, + { + id: "method", + content: t("pages.logs.table.headers.method"), + render: (row) => ( + + {row.method || "-"} + + ), + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => ( + + {row.message || "-"} + + ), + }, + ]; + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + dispatch(setRowsPerPage({ value: Number(e.target.value), table: "logs" })); + setPage(0); + }; + + return ( + + + + + ); +}; diff --git a/client/src/Pages/Logs/index.tsx b/client/src/Pages/Logs/index.tsx new file mode 100644 index 000000000..2642e177b --- /dev/null +++ b/client/src/Pages/Logs/index.tsx @@ -0,0 +1,29 @@ +import { BasePage, Tabs, Tab } from "@/Components/v2/design-elements"; +import { TabLogs } from "@/Pages/Logs/TabLogs"; +import { TabQueue } from "@/Pages/Logs/TabQueue"; +import { TabDiagnostics } from "@/Pages/Logs/TabDiagnostics"; + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +const LogsPage = () => { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(0); + return ( + + setActiveTab(newValue)} + > + + + + + {activeTab === 0 && } + {activeTab === 1 && } + {activeTab === 2 && } + + ); +}; + +export default LogsPage; diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/old.jsx similarity index 100% rename from client/src/Pages/Logs/index.jsx rename to client/src/Pages/Logs/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index d4a5619d6..22b5b446a 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -55,7 +55,7 @@ import ProtectedRoute from "../Components/v1/ProtectedRoute"; import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute"; import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; import BulkImport from "../Pages/Uptime/BulkImport/index.jsx"; -import Logs from "../Pages/Logs/index.jsx"; +import Logs from "../Pages/Logs"; import CreateMonitor from "@/Pages/CreateMonitor"; @@ -350,7 +350,9 @@ const Routes = () => { path="logs" element={ - + + + } /> diff --git a/client/src/Types/Log.ts b/client/src/Types/Log.ts new file mode 100644 index 000000000..b2038e31f --- /dev/null +++ b/client/src/Types/Log.ts @@ -0,0 +1,13 @@ +export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const; +export type LogLevel = (typeof LOG_LEVELS)[number]; +export const LOG_LEVEL_OPTIONS = ["all", ...LOG_LEVELS] as const; +export type LogLevelOption = (typeof LOG_LEVEL_OPTIONS)[number]; +export interface Log { + message: string; + service?: string; + method?: string; + details?: Record; + stack?: string; + level: LogLevel; + timestamp: string; +} diff --git a/client/src/Types/state.ts b/client/src/Types/state.ts index 137d67b66..1575fffb8 100644 --- a/client/src/Types/state.ts +++ b/client/src/Types/state.ts @@ -19,6 +19,9 @@ export interface UIState { infrastructure: { rowsPerPage: number; }; + logs: { + rowsPerPage: number; + }; sidebar: { collapsed: boolean; }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 644057425..6ec628238 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -777,6 +777,32 @@ "title": "An infrastructure monitor is used to:" } }, + "logs": { + "tabs": { + "diagnostics": "Diagnostics", + "logs": "Logs", + "queue": "Job queue" + }, + "logLevelSelect": { + "label": "Log level" + }, + "noLogs": "No logs found", + "table": { + "headers": { + "timestamp": "Timestamp", + "level": "Level", + "service": "Service", + "method": "Method", + "monitorId": "Monitor ID", + "runCount": "Run count", + "failCount": "Fail count", + "lastRunAt": "Last run at", + "lockedAt": "Locked at", + "lastFinisehdAt": "Last finished at", + "lastRunTook": "Last run took" + } + } + }, "maintenanceWindow": { "fallback": { "actionButton": "Let's create your first maintenance window!", diff --git a/client/vite.config.ts b/client/vite.config.ts index 3d8d66335..f381eb5ed 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export default defineConfig(({}) => { - let version = "3.2.1"; + let version = "3.3"; return { base: "/", From b0686c0576d46a8b4e2d3c279aadab340b710b9b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 5 Feb 2026 21:40:36 +0000 Subject: [PATCH 2/6] queue tab --- .../Components/v2/design-elements/Table.tsx | 4 + client/src/Pages/Logs/Metrics.tsx | 50 ++++++ client/src/Pages/Logs/TabQueue.tsx | 46 ++++- client/src/Pages/Logs/TableJobs.tsx | 164 ++++++++++++++++++ client/src/Pages/Logs/TableLogs.tsx | 28 +-- client/src/Types/Queue.ts | 40 +++++ client/src/Utils/TimeUtils.ts | 6 + client/src/locales/en.json | 22 ++- 8 files changed, 335 insertions(+), 25 deletions(-) create mode 100644 client/src/Pages/Logs/Metrics.tsx create mode 100644 client/src/Pages/Logs/TableJobs.tsx create mode 100644 client/src/Types/Queue.ts diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index aea8b40a0..35cd4512f 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -29,6 +29,7 @@ import type { TablePaginationOwnProps } from "@mui/material/TablePagination"; import { useTranslation } from "react-i18next"; import { useState, Fragment, useEffect } from "react"; import { useTheme } from "@mui/material/styles"; +import type { SxProps, Theme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; export type Header = { @@ -47,6 +48,7 @@ type DataTableProps = renderExpandedContent?: (row: T) => React.ReactNode; emptyViewText?: string; emptyViewPositive?: boolean; + getRowSx?: (row: T) => SxProps; }; export function DataTable< @@ -64,6 +66,7 @@ export function DataTable< renderExpandedContent, emptyViewText, emptyViewPositive, + getRowSx, }: DataTableProps) { const theme = useTheme(); const [expanded, setExpanded] = useState<(string | number) | null>(null); @@ -204,6 +207,7 @@ export function DataTable< { if (expandableRows) handleExpand(row); diff --git a/client/src/Pages/Logs/Metrics.tsx b/client/src/Pages/Logs/Metrics.tsx new file mode 100644 index 000000000..8f96ec5ac --- /dev/null +++ b/client/src/Pages/Logs/Metrics.tsx @@ -0,0 +1,50 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/v2/design-elements"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import type { QueueMetrics } from "@/Types/Queue"; + +interface MetricsProps { + metrics: QueueMetrics | null; +} + +export const Metrics = ({ metrics }: MetricsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + if (!metrics) { + return null; + } + + return ( + + + 0 ? "success" : undefined} + title={t("pages.logs.metrics.activeJobs")} + subtitle={String(metrics.activeJobs)} + /> + 0 ? "error" : undefined} + title={t("pages.logs.metrics.failingJobs")} + subtitle={String(metrics.failingJobs)} + /> + + 0 ? "error" : undefined} + title={t("pages.logs.metrics.totalFailures")} + subtitle={String(metrics.totalFailures)} + /> + + ); +}; diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx index e327218d3..15efd56b7 100644 --- a/client/src/Pages/Logs/TabQueue.tsx +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -1,3 +1,47 @@ +import Stack from "@mui/material/Stack"; +import { TableJobs, TableFailedJobs } from "@/Pages/Logs/TableJobs"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useGet, usePost } from "@/Hooks/UseApi"; +import type { QueueData } from "@/Types/Queue"; +import { Metrics } from "@/Pages/Logs/Metrics"; +import { Button } from "@/Components/v2/inputs"; + export const TabQueue = () => { - return "Queue"; + const theme = useTheme(); + const { t } = useTranslation(); + const { + data: queueData, + // isLoading, + // error, + refetch, + } = useGet("/queue/all-metrics", {}, { refreshInterval: 5000 }); + + const { post, loading: isFlushing } = usePost(); + + const jobs = queueData?.jobs ?? []; + const metrics = queueData?.metrics ?? null; + + const handleFlushQueue = async () => { + await post("/queue/flush", {}); + refetch(); + }; + + return ( + + + + + + + + + ); }; diff --git a/client/src/Pages/Logs/TableJobs.tsx b/client/src/Pages/Logs/TableJobs.tsx new file mode 100644 index 000000000..e9608df9a --- /dev/null +++ b/client/src/Pages/Logs/TableJobs.tsx @@ -0,0 +1,164 @@ +import { Table } from "@/Components/v2/design-elements"; +import { Typography, useTheme } from "@mui/material"; +import prettyMilliseconds from "pretty-ms"; +import { formatTimestamp } from "@/Utils/TimeUtils"; + +import { useTranslation } from "react-i18next"; +import type { QueueJobFailure, QueueJobSummary, QueueMetrics } from "@/Types/Queue"; +import type { Header } from "@/Components/v2/design-elements"; + +type QueueJobWithId = QueueJobSummary & { id: string | number }; + +interface TableJobsProps { + jobs: QueueJobSummary[]; +} + +export const TableJobs = ({ jobs }: TableJobsProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const jobsWithId: QueueJobWithId[] = jobs.map((job) => ({ + ...job, + id: job.monitorId, + })); + + const headers: Header[] = [ + { + id: "id", + content: t("common.table.headers.monitorId"), + render: (row) => {row.monitorId}, + }, + { + id: "url", + content: t("common.table.headers.url"), + render: (row) => {row.monitorUrl}, + }, + { + id: "interval", + content: t("common.table.headers.interval"), + render: (row) => ( + {prettyMilliseconds(row.monitorInterval ?? 0)} + ), + }, + { + id: "type", + content: t("common.table.headers.type"), + render: (row) => {row.monitorType}, + }, + { + id: "active", + content: t("common.table.headers.active"), + render: (row) => {row.active.toString()}, + }, + { + id: "runCount", + content: t("pages.logs.table.headers.runCount"), + render: (row) => {row.runCount}, + }, + { + id: "failCount", + content: t("pages.logs.table.headers.failCount"), + render: (row) => {row.failCount}, + }, + { + id: "lastRun", + content: t("pages.logs.table.headers.lastRunAt"), + render: (row) => {formatTimestamp(row.lastRunAt)}, + }, + { + id: "lockedAt", + content: t("pages.logs.table.headers.lockedAt"), + render: (row) => {formatTimestamp(row.lockedAt)}, + }, + + { + id: "lastFinish", + content: t("pages.logs.table.headers.lastFinishedAt"), + render: (row) => {formatTimestamp(row.lastFinishedAt)}, + }, + { + id: "lastRunTook", + content: t("pages.logs.table.headers.lastRunTook"), + render: (row) => { + const value = row.lastRunTook ? prettyMilliseconds(row.lastRunTook) : "-"; + return {value}; + }, + }, + ]; + + return ( +
({ + ...(row.lockedAt && { + "& td": { backgroundColor: theme.palette.success.light }, + }), + })} + /> + ); +}; + +type QueueJobFailureWithId = QueueJobFailure & { id: string | number }; + +interface TableFailedJobsProps { + metrics: QueueMetrics | null; +} +export const TableFailedJobs = ({ metrics }: TableFailedJobsProps) => { + const { t } = useTranslation(); + if (!metrics) { + return null; + } + + const jobFailuresWithId: QueueJobFailureWithId[] = metrics.jobsWithFailures.map( + (job) => ({ + ...job, + id: job.monitorId, + }) + ); + + const headers: Header[] = [ + { + id: "monitorId", + content: t("common.table.headers.monitorId"), + render: (row) => { + return {row.monitorId}; + }, + }, + { + id: "monitorUrl", + content: t("common.table.headers.url"), + render: (row) => { + return {row.monitorUrl}; + }, + }, + { + id: "failCount", + content: t("pages.logs.table.headers.failCount"), + render: (row) => { + return {row.failCount}; + }, + }, + { + id: "failedAt", + content: t("pages.logs.table.headers.lastFailedAt"), + render: (row) => { + return {formatTimestamp(row.failedAt)}; + }, + }, + { + id: "failReason", + content: t("pages.logs.table.headers.failReason"), + render: (row) => { + return {row.failReason}; + }, + }, + ]; + + return ( +
+ ); +}; diff --git a/client/src/Pages/Logs/TableLogs.tsx b/client/src/Pages/Logs/TableLogs.tsx index e565739bd..dace0db43 100644 --- a/client/src/Pages/Logs/TableLogs.tsx +++ b/client/src/Pages/Logs/TableLogs.tsx @@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; import type { RootState } from "@/Types/state"; import { setRowsPerPage } from "@/Features/UI/uiSlice"; +import { formatTimestamp } from "@/Utils/TimeUtils"; type LogWithId = Log & { id: number }; @@ -42,12 +43,6 @@ const LevelBadge = ({ level }: { level: LogLevel }) => { ); }; -const formatTimestamp = (timestamp: string): string => { - if (!timestamp) return "-"; - const date = new Date(timestamp); - return date.toLocaleString(); -}; - export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -60,7 +55,7 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => id: "timestamp", content: t("pages.logs.table.headers.timestamp"), render: (row) => ( - + {formatTimestamp(row.timestamp)} ), @@ -73,17 +68,13 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => { id: "service", content: t("pages.logs.table.headers.service"), - render: (row) => ( - {row.service || "-"} - ), + render: (row) => {row.service || "-"}, }, { id: "method", content: t("pages.logs.table.headers.method"), render: (row) => ( - - {row.method || "-"} - + {row.method || "-"} ), }, { @@ -91,13 +82,10 @@ export const TableLogs = ({ logs, logCount, page, setPage }: TableLogsProps) => content: t("common.table.headers.message"), render: (row) => ( {row.message || "-"} diff --git a/client/src/Types/Queue.ts b/client/src/Types/Queue.ts new file mode 100644 index 000000000..77a770aeb --- /dev/null +++ b/client/src/Types/Queue.ts @@ -0,0 +1,40 @@ +import type { MonitorType } from "./Monitor"; + +export interface QueueJobFailure { + monitorId: string | number; + monitorUrl: string | null; + monitorType: MonitorType | null; + failedAt: number | null; + failCount: number; + failReason: string | null; +} + +export interface QueueMetrics { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: QueueJobFailure[]; + totalRuns: number; + totalFailures: number; +} + +export interface QueueJobSummary { + monitorId: string | number; + monitorUrl: string | null; + monitorType: MonitorType | null; + monitorInterval: number | null; + active: boolean; + lockedAt: number | null; + runCount: number; + failCount: number; + failReason: string | null; + lastRunAt: number | null; + lastFinishedAt: number | null; + lastRunTook: number | null; + lastFailedAt: number | null; +} + +export interface QueueData { + jobs: QueueJobSummary[]; + metrics: QueueMetrics; +} diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts index a19a0bf8b..abd56ce44 100644 --- a/client/src/Utils/TimeUtils.ts +++ b/client/src/Utils/TimeUtils.ts @@ -50,3 +50,9 @@ export const tooltipDateFormatLookup = (range: string) => { } return format; }; + +export const formatTimestamp = (timestamp: string | number | null): string => { + if (!timestamp) return "-"; + const date = new Date(timestamp); + return date.toLocaleString(); +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 6ec628238..307161905 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -199,7 +199,8 @@ "save": "Save", "test": "Test", "testNotifications": "Test notifications", - "toggleTheme": "Toggles light & dark" + "toggleTheme": "Toggles light & dark", + "flushQueue": "Flush queue" }, "charts": { "labels": { @@ -230,9 +231,13 @@ "dateTime": "Date & time", "message": "Message", "monitor": "Monitor", + "monitorId": "Monitor ID", "name": "Name", "status": "Status", - "type": "Type" + "type": "Type", + "url": "Url", + "interval": "Interval", + "active": "Active" } }, "toasts": { @@ -787,6 +792,13 @@ "label": "Log level" }, "noLogs": "No logs found", + "metrics": { + "jobs": "Jobs", + "activeJobs": "Active jobs", + "failingJobs": "Failing jobs", + "totalRuns": "Total runs", + "totalFailures": "Total failures" + }, "table": { "headers": { "timestamp": "Timestamp", @@ -798,8 +810,10 @@ "failCount": "Fail count", "lastRunAt": "Last run at", "lockedAt": "Locked at", - "lastFinisehdAt": "Last finished at", - "lastRunTook": "Last run took" + "lastFinishedAt": "Last finished at", + "lastRunTook": "Last run took", + "lastFailedAt": "Last failed at", + "failReason": "Fail reason" } } }, From 9974fa401083f78b89daae499fa28d4002469cba Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Feb 2026 17:24:16 +0000 Subject: [PATCH 3/6] headers --- client/src/Pages/Logs/TabQueue.tsx | 13 +++++++++++++ client/src/locales/en.json | 2 ++ 2 files changed, 15 insertions(+) diff --git a/client/src/Pages/Logs/TabQueue.tsx b/client/src/Pages/Logs/TabQueue.tsx index 15efd56b7..7cfd15f0e 100644 --- a/client/src/Pages/Logs/TabQueue.tsx +++ b/client/src/Pages/Logs/TabQueue.tsx @@ -1,4 +1,5 @@ import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; import { TableJobs, TableFailedJobs } from "@/Pages/Logs/TableJobs"; import { useTheme } from "@mui/material"; @@ -31,7 +32,19 @@ export const TabQueue = () => { return ( + + {t("pages.logs.jobQueue")} + + + {t("pages.logs.failedJobs")} + - - - ); -}; - -export default Diagnostics; diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js deleted file mode 100644 index a3b75631f..000000000 --- a/client/src/Pages/Logs/Diagnostics/utils/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -export const getPercentage = (value, total) => { - if (!value || !total) return 0; - return (value / total) * 100; -}; - -export const formatBytes = (bytes) => { - if (!bytes) return "N/A"; - - if (bytes === 0) return "0 Bytes"; - - if (bytes >= 1024 * 1024 * 1024) { - return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; - } - - return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; -}; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx deleted file mode 100644 index d0319a387..000000000 --- a/client/src/Pages/Logs/Logs/index.jsx +++ /dev/null @@ -1,176 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import Typography from "@mui/material/Typography"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; - -import { useFetchLogs } from "../../../Hooks/logHooks.js"; -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; - -const LevelBadge = ({ level }) => { - const theme = useTheme(); - - const levelColors = { - info: theme.palette.success.main, - warn: theme.palette.warning.main, - error: theme.palette.error.main, - debug: theme.palette.accent.main, - }; - - const color = levelColors[level] || theme.palette.primary.contrastText; - - return ( - - {level} - - ); -}; - -const formatTimestamp = (timestamp) => { - if (!timestamp) return "-"; - const date = new Date(timestamp); - return date.toLocaleString(); -}; - -const Logs = () => { - const [logLevel, setLogLevel] = useState("all"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(15); - - const theme = useTheme(); - const { t } = useTranslation(); - const [logs, isLoading, error] = useFetchLogs(); - - const LOG_LEVELS = [ - { _id: "all", name: t("logsPage.logLevelSelect.values.all") }, - { _id: "info", name: t("logsPage.logLevelSelect.values.info") }, - { _id: "warn", name: t("logsPage.logLevelSelect.values.warn") }, - { _id: "error", name: t("logsPage.logLevelSelect.values.error") }, - { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") }, - ]; - - const headers = [ - { - id: "timestamp", - content: t("logsPage.table.timestamp"), - render: (row) => ( - - {formatTimestamp(row.timestamp)} - - ), - }, - { - id: "level", - content: t("logsPage.table.level"), - render: (row) => , - }, - { - id: "service", - content: t("logsPage.table.service"), - render: (row) => ( - {row.service || "-"} - ), - }, - { - id: "method", - content: t("logsPage.table.method"), - render: (row) => ( - {row.method || "-"} - ), - }, - { - id: "message", - content: t("logsPage.table.message"), - render: (row) => ( - - {row.message || "-"} - - ), - }, - ]; - - const filteredLogs = logs - ?.filter((log) => { - if (logLevel === "all") return true; - return log.level === logLevel; - }) - .reverse() - .map((log, idx) => ({ ...log, id: idx })); - - const paginatedLogs = filteredLogs?.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ); - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - const handleLogLevelChange = (e) => { - setLogLevel(e.target.value); - setPage(0); - }; - - return ( - - - {t("logsPage.logLevelSelect.title")} -