diff --git a/client/src/components/checks/CardCheckDetails.tsx b/client/src/components/checks/CardCheckDetails.tsx
new file mode 100644
index 000000000..3b8811ec5
--- /dev/null
+++ b/client/src/components/checks/CardCheckDetails.tsx
@@ -0,0 +1,227 @@
+import Stack from "@mui/material/Stack";
+import Grid from "@mui/material/Grid";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Link from "@mui/material/Link";
+import { Link as RouterLink } from "react-router-dom";
+import { StatusLabel, ValueLabel } from "@/components/design-elements";
+import { BaseBox } from "@/components/design-elements";
+
+import { useAppSelector } from "@/hooks/AppHooks";
+import type { ICheck } from "@/types/check";
+import type { MonitorStatus } from "@/types/monitor";
+import { formatDateWithTz } from "@/utils/TimeUtils";
+import { useTheme } from "@mui/material/styles";
+import { getMonitorPath } from "@/utils/MonitorUtils";
+
+type CheckDetailsCardProps = {
+ check: ICheck;
+};
+
+export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => {
+ const theme = useTheme();
+ const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
+ const phases = check?.timings?.phases;
+ const phaseKeys = [
+ "wait",
+ "dns",
+ "tcp",
+ "tls",
+ "request",
+ "firstByte",
+ "download",
+ "total",
+ ] as const;
+
+ const status =
+ (check?.status as MonitorStatus) ?? ("initializing" as MonitorStatus);
+ const httpStatusCode = Number(check?.httpStatusCode ?? NaN);
+ const httpValue: "positive" | "neutral" | "negative" = Number.isFinite(
+ httpStatusCode
+ )
+ ? httpStatusCode < 300
+ ? "positive"
+ : httpStatusCode < 400
+ ? "neutral"
+ : "negative"
+ : "neutral";
+ const httpText = Number.isFinite(httpStatusCode)
+ ? String(httpStatusCode)
+ : "-";
+ const typeText = check?.metadata?.type ?? "-";
+ const checkedAtText = check?.createdAt
+ ? formatDateWithTz(check.createdAt, "ddd, MMM D, YYYY, HH:mm A", uiTimezone)
+ : "-";
+ const messageText = check?.message ?? "-";
+ const getPhaseMs = (key: (typeof phaseKeys)[number]) => {
+ const v = Number((phases as any)?.[key] ?? NaN);
+ return Number.isFinite(v) ? `${Math.floor(Math.max(0, v))} ms` : "-";
+ };
+
+ return (
+
+
+
+ Check details
+ {`Total request time: ${getPhaseMs("total")}`}
+ {(() => {
+ const href = `/${getMonitorPath(check.metadata.type)}/${check.metadata.monitorId}`;
+ return href ? (
+
+
+ View monitor details
+
+
+ ) : null;
+ })()}
+
+ {/* Basic info section */}
+
+
+ Basic check info
+
+
+
+ Status
+
+
+
+
+
+
+
+
+
+ HTTP Code
+
+
+
+
+
+
+
+
+
+ Type
+
+
+
+ {typeText}
+
+
+
+
+ Check time
+
+
+
+ {checkedAtText}
+
+
+
+ Message
+
+
+
+ {messageText}
+
+
+ {/* Timing phases section */}
+
+
+ Timing phases
+
+
+
+
+ Wait
+
+
+
+ {getPhaseMs("wait")}
+
+
+
+
+ DNS
+
+
+
+ {getPhaseMs("dns")}
+
+
+
+
+ TCP
+
+
+
+ {getPhaseMs("tcp")}
+
+
+
+
+ TLS
+
+
+
+ {getPhaseMs("tls")}
+
+
+
+
+ Request
+
+
+
+ {getPhaseMs("request")}
+
+
+
+
+ First Byte
+
+
+
+ {getPhaseMs("firstByte")}
+
+
+
+
+ Download
+
+
+
+ {getPhaseMs("download")}
+
+
+
+
+ Total
+
+
+
+ {getPhaseMs("total")}
+
+
+
+
+ );
+};
diff --git a/client/src/components/checks/CardChecktDetails.tsx b/client/src/components/checks/CardChecktDetails.tsx
deleted file mode 100644
index 245cb9b20..000000000
--- a/client/src/components/checks/CardChecktDetails.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import Stack from "@mui/material/Stack";
-import Grid from "@mui/material/Grid";
-import Box from "@mui/material/Box";
-import Typography from "@mui/material/Typography";
-import { StatusLabel, ValueLabel } from "@/components/design-elements";
-import { BaseBox } from "@/components/design-elements";
-
-import { useAppSelector } from "@/hooks/AppHooks";
-import type { ICheck } from "@/types/check";
-import type { MonitorStatus } from "@/types/monitor";
-import { formatDateWithTz } from "@/utils/TimeUtils";
-
-type CheckDetailsCardProps = {
- check: ICheck;
-};
-
-export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => {
- const uiTimezone = useAppSelector((state: any) => state.ui.timezone);
- const phases = check?.timings?.phases;
- const phaseKeys = [
- "wait",
- "dns",
- "tcp",
- "tls",
- "request",
- "firstByte",
- "download",
- ] as const;
- const totalMs = phases
- ? phaseKeys.reduce(
- (sum, key) => sum + Math.max(0, Number((phases as any)[key] ?? 0)),
- 0
- )
- : Number(check.responseTime ?? 0);
-
- return (
-
-
- Check details
-
- {`Total request time: ${check.timings.phases.total} ms`}
-
-
-
-
-
- Status
-
-
-
-
-
-
-
-
-
-
-
- HTTP Code
-
-
-
-
-
-
-
-
-
- Type
-
-
-
-
- {check.metadata?.type || "-"}
-
-
-
-
- {Math.round(totalMs)} ms
-
-
-
- Checked At
-
-
-
-
- {formatDateWithTz(
- check.createdAt,
- "ddd, MMM D, YYYY, HH:mm A",
- uiTimezone
- )}
-
-
-
-
- Message
-
-
-
- {check.message || "-"}
-
-
-
-
- );
-};
diff --git a/client/src/components/checks/ChartTiming.tsx b/client/src/components/checks/ChartTiming.tsx
index 3eb3b1c5c..b935cd13d 100644
--- a/client/src/components/checks/ChartTiming.tsx
+++ b/client/src/components/checks/ChartTiming.tsx
@@ -3,6 +3,7 @@ import { PieChart, Pie, ResponsiveContainer, Tooltip, Legend } from "recharts";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { alpha, useTheme } from "@mui/material/styles";
+import { useState } from "react";
import { BaseBox } from "@/components/design-elements";
type TimingsChartProps = { check: ICheck | null };
@@ -26,13 +27,15 @@ const PHASE_ORDER: (keyof CheckTimingPhases)[] = [
const buildTimingSegments = (
phases: CheckTimingPhases | undefined,
- stroke: string
+ stroke: string,
+ hoveredIndex: number | null,
+ hoverFill: string
): TimingSegment[] => {
if (!phases) return [];
- return PHASE_ORDER.map((key) => ({
+ return PHASE_ORDER.map((key, i) => ({
name: String(key),
value: Math.max(0, Number((phases as any)[key] ?? 0)),
- fill: "transparent",
+ fill: hoveredIndex === i ? hoverFill : "transparent",
stroke,
}));
};
@@ -42,68 +45,21 @@ export const TimingsChart = ({ check }: TimingsChartProps) => {
const theme = useTheme();
const strokeColor = alpha(theme.palette.primary.main, 0.8);
- const segments = buildTimingSegments(check?.timings?.phases, strokeColor);
- if (!segments.length) return null;
- const LABEL_THRESHOLD = 0.05;
-
- const renderLabel = ({
- cx,
- cy,
- midAngle,
- outerRadius,
- name,
- value,
- percent,
- }: any) => {
- if (!percent || percent < LABEL_THRESHOLD) return null;
- const RAD = Math.PI / 180;
- const r = (outerRadius || 0) + 24;
- const x = cx + r * Math.cos(-midAngle * RAD);
- const y = cy + r * Math.sin(-midAngle * RAD);
- const text = `${name}: ${Math.round(Number(value) || 0)} ms`;
- return (
- cx ? "start" : "end"}
- dominantBaseline="central"
- style={{ fontSize: 12 }}
- >
- {text}
-
- );
- };
-
- const renderLegend = () => (
-
- {segments.map((s) => (
-
-
-
- {s.name}: {Math.round(Number(s.value) || 0)} ms
-
-
- ))}
-
+ const [hoveredIndex, setHoveredIndex] = useState(null);
+ const segments = buildTimingSegments(
+ check?.timings?.phases,
+ strokeColor,
+ hoveredIndex,
+ theme.palette.primary.main
);
+ if (!segments.length) return null;
return (
-
+
Request timings
- {`Total request time: ${check.timings.phases.total} ms`}
+ {`Total request time: ${Math.floor(Number(check?.timings?.phases?.total ?? 0))} ms`}
@@ -113,18 +69,22 @@ export const TimingsChart = ({ check }: TimingsChartProps) => {
nameKey="name"
stroke="none"
minAngle={4}
- label={renderLabel}
- labelLine={{ stroke: strokeColor, strokeWidth: 1, opacity: 0.6 }}
+ onMouseEnter={(_, index: number) => setHoveredIndex(index)}
+ onMouseLeave={() => setHoveredIndex(null)}
/>
`${Math.round(value as number)} ms`}
+ formatter={(value) => `${Math.floor(value as number)} ms`}
/>
diff --git a/client/src/components/checks/index.tsx b/client/src/components/checks/index.tsx
index e594e2429..f36a2fdf8 100644
--- a/client/src/components/checks/index.tsx
+++ b/client/src/components/checks/index.tsx
@@ -1,2 +1,2 @@
export { TimingsChart } from "./ChartTiming";
-export { CheckDetailsCard } from "./CardChecktDetails";
+export { CheckDetailsCard } from "./CardCheckDetails";
diff --git a/client/src/components/design-elements/BasePage.tsx b/client/src/components/design-elements/BasePage.tsx
index 57bf017db..a5ef25d7f 100644
--- a/client/src/components/design-elements/BasePage.tsx
+++ b/client/src/components/design-elements/BasePage.tsx
@@ -123,7 +123,7 @@ export const MonitorBasePageWithStates = ({
const showLoading = loading && (!items || items.length === 0);
- if (isEmpty(items)) {
+ if (!loading && isEmpty(items)) {
return (
{
await connectDatabase();
@@ -38,13 +39,28 @@ const createApp = async () => {
const routes = initRoutes(controllers, app);
// expose services for route-level access (e.g., in me routes)
app.set("services", services);
- // Swagger UI at /api-docs
+
try {
- // Load OpenAPI doc from project root within server folder
- const openapiPath = path.resolve(process.cwd(), "openapi.json");
- const openapiJson = JSON.parse(fs.readFileSync(openapiPath, "utf-8"));
- app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openapiJson));
- indexLogger.info("Swagger UI available at /api-docs");
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
+ const candidates = [
+ path.resolve(process.cwd(), "openapi.json"),
+ path.resolve(process.cwd(), "server/openapi.json"),
+ path.resolve(__dirname, "../openapi.json"),
+ ];
+ const existing = candidates.find((p) => fs.existsSync(p));
+ if (!existing) {
+ indexLogger.error(
+ "OpenAPI file not found. Checked: " + candidates.join(", ")
+ );
+ } else {
+ const openapiJson = JSON.parse(fs.readFileSync(existing, "utf-8"));
+ const setup = swaggerUi.setup(openapiJson);
+ app.use("/api-docs", swaggerUi.serve, setup);
+
+ indexLogger.info(
+ `Swagger UI available at /api-docs and /api/v1/api-docs (spec: ${existing})`
+ );
+ }
} catch (e) {
indexLogger.error("Failed to load OpenAPI document for Swagger UI", e);
}
@@ -53,7 +69,6 @@ const createApp = async () => {
status: "OK",
});
});
-
const port = 52345;
app.listen(port, "0.0.0.0", () => {
indexLogger.info(`Server is running on http://localhost:${port}`);