Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions client/src/Components/v2/monitors/MonitorStatBoxes.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Stack from "@mui/material/Stack";
import { StatBox } from "@/Components/v2/design-elements";

import prettyMilliseconds from "pretty-ms";
import { useTheme } from "@mui/material/styles";
import type { MonitorStats, Monitor } from "@/Types/Monitor";
import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
import { getStatusPalette } from "@/Utils/MonitorUtils";
import { useTranslation } from "react-i18next";

Expand Down Expand Up @@ -32,11 +32,16 @@ export const MonitorStatBoxes = ({

// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
const timeSinceLastCheck = Date.now() - timeOfLastCheck;
const timeSinceLastCheck = Date.now() - timeOfLastCheck || 0;

const streakTime = getHumanReadableDuration(timeSinceLastFailure);
const options = {
secondsDecimalDigits: 0,
millisecondsDecimalDigits: 0,
};

const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck);
const streakTime = prettyMilliseconds(timeSinceLastFailure, options);

const lastCheckTime = prettyMilliseconds(timeSinceLastCheck, options);
const palette = getStatusPalette(monitor?.status);

return (
Expand All @@ -55,7 +60,7 @@ export const MonitorStatBoxes = ({
/>
<StatBox
title={t("pages.common.monitors.statBoxes.lastResponseTime")}
subtitle={monitorStats?.lastResponseTime + " ms"}
subtitle={prettyMilliseconds(monitorStats?.lastResponseTime ?? 0)}
/>

{monitor?.type === "http" && (
Expand Down
7 changes: 5 additions & 2 deletions client/src/Pages/Incidents/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Incident, IncidentSummaryItem } from "@/Types/Incident";
import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
import prettyMilliseconds from "pretty-ms";

type IncidentLike = Pick<Incident, "startTime" | "endTime" | "status">;

Expand All @@ -24,5 +24,8 @@ export const getIncidentsDuration = (incident: IncidentLike | IncidentSummaryIte
return "-";
}

return getHumanReadableDuration(durationMs);
return prettyMilliseconds(durationMs, {
secondsDecimalDigits: 0,
millisecondsDecimalDigits: 0,
});
};
11 changes: 6 additions & 5 deletions client/src/Pages/Infrastructure/Details/Components/Gauges.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Stack from "@mui/material/Stack";
import { DetailGauge } from "@/Components/v2/design-elements";

import prettyBytes from "pretty-bytes";
import { useTranslation } from "react-i18next";
import { getGbs, getFrequency } from "@/Utils/InfraUtils";
import { getFrequency } from "@/Utils/InfraUtils";
import { useTheme } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { CheckSnapshot } from "@/Types/Check";
Expand Down Expand Up @@ -30,9 +31,9 @@ export const InfraDetailsGauges = ({
title={t("pages.infrastructure.gauges.memory.title")}
progress={(snapshot?.memory?.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.memory.upperLabel")}
upperValue={getGbs(snapshot?.memory?.used_bytes || 0)}
upperValue={prettyBytes(snapshot?.memory?.used_bytes || 0)}
lowerLabel={t("pages.infrastructure.gauges.memory.lowerLabel")}
lowerValue={getGbs(snapshot?.memory?.total_bytes || 0)}
lowerValue={prettyBytes(snapshot?.memory?.total_bytes || 0)}
/>
<DetailGauge
title={t("pages.infrastructure.gauges.cpu.title")}
Expand All @@ -50,9 +51,9 @@ export const InfraDetailsGauges = ({
title={t("pages.infrastructure.gauges.disk.title", { idx })}
progress={(disk.usage_percent || 0) * 100}
upperLabel={t("pages.infrastructure.gauges.disk.upperLabel")}
upperValue={getGbs(disk?.used_bytes || 0)}
upperValue={prettyBytes(disk?.used_bytes || 0)}
lowerLabel={t("pages.infrastructure.gauges.disk.lowerLabel")}
lowerValue={getGbs(disk?.total_bytes || 0)}
lowerValue={prettyBytes(disk?.total_bytes || 0)}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import Stack from "@mui/material/Stack";
import { StatBox } from "@/Components/v2/design-elements";

import prettyBytes from "pretty-bytes";
import { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import type { Monitor } from "@/Types/Monitor";
import {
getAvgTemp,
getCores,
getFrequency,
getGbs,
getDiskTotalGbs,
getOsAndPlatform,
} from "@/Utils/InfraUtils";
import { getAvgTemp, getCores, getFrequency, getOsAndPlatform } from "@/Utils/InfraUtils";

export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
const { t } = useTranslation();
Expand All @@ -24,8 +18,9 @@ export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
const cpuFrequency = getFrequency(latestCheck?.cpu?.frequency);
const cpuTemps = latestCheck?.cpu?.temperature ?? [];
const cpuTemperature = getAvgTemp(cpuTemps);
const memoryTotalBytes = getGbs(latestCheck?.memory?.total_bytes);
const diskTotalBytes = getDiskTotalGbs(latestCheck?.disk);
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
const diskTotalBytes =
latestCheck?.disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0;
const os = getOsAndPlatform(latestCheck?.host);

const platform = latestCheck?.host?.platform ?? undefined;
Expand Down Expand Up @@ -58,11 +53,11 @@ export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
/>
<StatBox
title={t("pages.infrastructure.statBoxes.memory")}
subtitle={memoryTotalBytes.toString()}
subtitle={prettyBytes(memoryTotalBytes)}
/>
<StatBox
title={t("pages.infrastructure.statBoxes.disk")}
subtitle={diskTotalBytes.toString()}
subtitle={prettyBytes(diskTotalBytes)}
/>
<StatBox
title={t("pages.infrastructure.statBoxes.os")}
Expand Down
22 changes: 5 additions & 17 deletions client/src/Pages/Logs/components/StatGauges.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Stack from "@mui/material/Stack";
import { DetailGauge } from "@/Components/v2/design-elements";

import { getPercentage, formatPercentageFromWhole } from "@/Utils/FormatUtils";
import prettyBytes from "pretty-bytes";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material";
Expand All @@ -10,17 +11,6 @@ interface StatGaugesProps {
diagnostics: Diagnostics | null;
}

const getPercentage = (value: number, total: number) => {
if (!value || !total) return 0;
return (value / total) * 100;
};

const formatPercentage = new Intl.NumberFormat("en-US", {
style: "percent",
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});

export const StatGauges = ({ diagnostics }: StatGaugesProps) => {
const theme = useTheme();
const { t } = useTranslation();
Expand Down Expand Up @@ -51,33 +41,31 @@ export const StatGauges = ({ diagnostics }: StatGaugesProps) => {
<DetailGauge
title={t("pages.logs.diagnostics.gauges.heapAllocation")}
progress={heapTotalSize}
upperValue={formatPercentage.format(heapTotalSize / 100)}
upperValue={formatPercentageFromWhole(heapTotalSize)}
lowerLabel={t("pages.logs.diagnostics.gauges.total")}
lowerValue={prettyBytes(diagnostics.v8HeapStats?.heapSizeLimitBytes ?? 0)}
/>
<DetailGauge
title={t("pages.logs.diagnostics.gauges.heapUsage")}
progress={heapUsedSize}
upperLabel={t("pages.logs.diagnostics.gauges.availableMemoryPercentage")}
upperValue={formatPercentage.format(heapUsedSize / 100)}
upperValue={formatPercentageFromWhole(heapUsedSize)}
lowerLabel={t("pages.logs.diagnostics.gauges.used")}
lowerValue={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
/>
<DetailGauge
title={t("pages.logs.diagnostics.gauges.heapUtilization")}
progress={actualHeapUsed}
upperLabel={t("pages.logs.diagnostics.gauges.allocatedPercentage")}
upperValue={formatPercentage.format(actualHeapUsed / 100)}
upperValue={formatPercentageFromWhole(actualHeapUsed)}
lowerLabel={t("pages.logs.diagnostics.gauges.total")}
lowerValue={prettyBytes(diagnostics.v8HeapStats?.usedHeapSizeBytes ?? 0)}
/>
<DetailGauge
title={t("pages.logs.diagnostics.gauges.instantCpuUsage")}
progress={diagnostics.cpuUsage?.usagePercentage ?? 0}
upperLabel={t("pages.logs.diagnostics.gauges.usedSPercentage")}
upperValue={formatPercentage.format(
(diagnostics.cpuUsage?.usagePercentage ?? 0) / 100
)}
upperValue={formatPercentageFromWhole(diagnostics.cpuUsage?.usagePercentage ?? 0)}
/>
</Stack>
);
Expand Down
7 changes: 4 additions & 3 deletions client/src/Pages/Maintenance/MaintenanceWindowTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Typography from "@mui/material/Typography";
import prettyMilliseconds from "pretty-ms";
import { Table, ValueLabel } from "@/Components/v2/design-elements";
import { Pagination } from "@/Components/v2/design-elements/Table";
import { ActionsMenu } from "@/Components/v2/actions-menu";
import { DialogInput } from "@/Components/v2/inputs/Dialog";

import prettyMilliseconds from "pretty-ms";
import { useTheme } from "@mui/material";
import type { Header } from "@/Components/v2/design-elements/Table";
import type { ActionMenuItem } from "@/Components/v2/actions-menu";
Expand All @@ -15,7 +15,6 @@ import { useSelector, useDispatch } from "react-redux";
import type { RootState } from "@/Types/state";
import Box from "@mui/material/Box";
import { setRowsPerPage } from "@/Features/UI/uiSlice";
import { formatDurationRounded } from "@/Utils/timeUtilsLegacy";
import dayjs from "dayjs";
import { useState } from "react";
import { useDelete, usePatch } from "@/Hooks/UseApi";
Expand Down Expand Up @@ -153,7 +152,9 @@ export const MaintenanceWindowTable = ({
id: "repeat",
content: t("pages.maintenanceWindow.table.headers.repeat"),
render: (row) =>
row.repeat === 0 ? t("common.labels.na") : formatDurationRounded(row.repeat),
row.repeat === 0
? t("common.labels.na")
: prettyMilliseconds(row.repeat, { verbose: true }),
},
{
id: "actions",
Expand Down
2 changes: 1 addition & 1 deletion client/src/Pages/Uptime/Details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type { RootState } from "@/Types/state";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import { t } from "i18next";

const certificateDateFormat = "MMM D, YYYY h A";
Expand Down
43 changes: 43 additions & 0 deletions client/src/Utils/FormatUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Intl.NumberFormat instance for percentage formatting.
* Reused across all formatting calls for performance.
*/
const percentageFormatter = new Intl.NumberFormat("en-US", {
style: "percent",
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});

/**
* Formats a decimal value as a percentage string.
* @param value - Decimal value (e.g., 0.75 for 75%)
* @returns Formatted percentage string (e.g., "75.0%")
* @example
* formatPercentage(0.75) // "75.0%"
* formatPercentage(1) // "100.0%"
* formatPercentage(0.5432) // "54.3%"
*/
export const formatPercentage = (value: number): string => {
if (typeof value !== "number" || Number.isNaN(value)) {
return "0.0%";
}
return percentageFormatter.format(value);
};

/**
* Formats a whole number percentage value as a percentage string.
* @param value - Whole number percentage (e.g., 75 for 75%)
* @returns Formatted percentage string (e.g., "75.0%")
* @example
* formatPercentageFromWhole(75) // "75.0%"
* formatPercentageFromWhole(100) // "100.0%"
* formatPercentageFromWhole(54.32) // "54.3%"
*/
export const formatPercentageFromWhole = (value: number): string => {
return formatPercentage(value / 100);
};

export const getPercentage = (value: number, total: number) => {
if (!value || !total) return 0;
return (value / total) * 100;
};
28 changes: 1 addition & 27 deletions client/src/Utils/InfraUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CheckDiskInfo, CheckHostInfo } from "@/Types/Check";
import type { CheckHostInfo } from "@/Types/Check";

export const getFrequency = (frequency: number | undefined): string => {
if (!frequency) return "N/A";
Expand All @@ -18,32 +18,6 @@ export const getAvgTemp = (temps: number[]): string => {
return `${avgTemp?.toFixed(2)} °C`;
};

export const getGbs = (bytes: number | undefined): string => {
if (!bytes) {
return "N/A";
}
if (bytes === 0) {
return "0 GB";
}

const GB = bytes / (1024 * 1024 * 1024);
const MB = bytes / (1024 * 1024);

if (GB >= 1) {
return GB.toFixed(2) + " GB";
} else {
return MB.toFixed(2) + " MB";
}
};

export const getDiskTotalGbs = (disk?: Partial<CheckDiskInfo>[]): string => {
if (!disk) {
return getGbs(0);
}
const totalBytes = disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0;
return getGbs(totalBytes);
};

export const getOsAndPlatform = (hostInfo: CheckHostInfo | undefined): string => {
if (!hostInfo) {
return "N/A";
Expand Down
13 changes: 9 additions & 4 deletions server/src/service/business/diagnosticService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ class DiagnosticService {
};

const used = process.memoryUsage();
const memoryUsage: Record<string, number> = {};
for (const key of Object.keys(used) as Array<keyof NodeJS.MemoryUsage>) {
memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB
}

// In MB
const memoryUsage: Record<keyof NodeJS.MemoryUsage, number> = {
rss: Math.round((used.rss / 1024 / 1024) * 100) / 100,
heapTotal: Math.round((used.heapTotal / 1024 / 1024) * 100) / 100,
heapUsed: Math.round((used.heapUsed / 1024 / 1024) * 100) / 100,
external: Math.round((used.external / 1024 / 1024) * 100) / 100,
arrayBuffers: Math.round((used.arrayBuffers / 1024 / 1024) * 100) / 100,
};

// CPU Usage
const cpuMetrics = await this.getCPUUsage();
Expand Down