"use client";
import type { CSSProperties } from "react";
import { Badge, Box, Group, RingProgress, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconArrowDown, IconArrowUp, IconClockPause, IconServer } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { formatNumber } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
import type { WidgetComponentProps } from "../definition";
import { getUsableWidgetQueryData } from "../common/query-state";
import { NoIntegrationDataError } from "../errors/no-data-integration";
import classes from "./component.module.css";
import { aggregateUptimeKumaDashboards } from "./aggregate";
const statVisibilityByOption = {
showTotalMonitors: "totalMonitors",
showUpCount: "upCount",
showDownCount: "downCount",
showPausedCount: "pausedCount",
} as const;
const statIcons = {
totalMonitors: IconServer,
upCount: IconArrowUp,
downCount: IconArrowDown,
pausedCount: IconClockPause,
} as const;
const statColorByKey = {
totalMonitors: "gray",
upCount: "green",
downCount: "red",
pausedCount: "yellow",
} as const;
const uptimeTierByThreshold = [
[99, "excellent"],
[95, "good"],
[0, "poor"],
] as const;
const uptimeColorByTier = {
excellent: "green",
good: "yellow",
poor: "red",
} as const;
const gridColsByWidth = [
{ minWidth: 380, cols: 4 },
{ minWidth: 260, cols: 2 },
{ minWidth: 0, cols: 2 },
] as const;
const ringSizeByWidth = [
{ minWidth: 400, size: 88 },
{ minWidth: 320, size: 72 },
{ minWidth: 220, size: 56 },
{ minWidth: 0, size: 44 },
] as const;
const iconSizeByWidth = [
{ minWidth: 320, size: 22 },
{ minWidth: 200, size: 18 },
{ minWidth: 0, size: 16 },
] as const;
const heroLayoutBySecondaryStats = {
false: classes.heroExpanded,
true: "",
} as const;
const heroVariantByRing = {
true: "",
false: classes.heroTextOnly,
} as const;
export default function UptimeKumaWidget({
integrationIds,
options,
width,
height,
displayMode = "compact",
}: WidgetComponentProps<"uptimeKuma">) {
if (integrationIds.length === 0) {
throw new NoIntegrationDataError();
}
return (
);
}
interface UptimeKumaContentProps {
integrationIds: string[];
options: WidgetComponentProps<"uptimeKuma">["options"];
width: number;
height: number;
displayMode: "compact" | "advanced";
}
function UptimeKumaContent({ integrationIds, options, width, height, displayMode }: UptimeKumaContentProps) {
const t = useI18n("widget.uptimeKuma");
const dashboardQuery = clientApi.widget.uptimeKuma.getDashboard.useQuery({ integrationIds });
const dashboardData = getUsableWidgetQueryData(dashboardQuery);
if (!dashboardData) return ;
const successfulDashboardData = dashboardData.flatMap((item) =>
item.dashboard === null ? [] : [{ ...item, dashboard: item.dashboard }],
);
const combined = aggregateUptimeKumaDashboards(successfulDashboardData.map((item) => item.dashboard));
const monitors = successfulDashboardData.flatMap((result) =>
result.dashboard.monitors.map((monitor) => ({
...monitor,
key: `${result.integrationId}:${monitor.id}`,
integrationName: result.integrationName,
})),
);
const uptimeValue = clampPercent(combined.averageUptimePercent);
const uptimeColor = getUptimeColor(uptimeValue);
const ringSize = getRingSize(width);
const iconSize = getIconSize(width);
const gridCols = getGridCols(width);
const isAdvanced = displayMode === "advanced";
const enabledStatKeys = Object.entries(statVisibilityByOption)
.filter(([optionKey]) => isAdvanced || options[optionKey as keyof typeof options])
.map(([, statKey]) => statKey);
const showHero = isAdvanced || options.showAverageUptime;
const showRing = isAdvanced || options.showUptimeRing;
const visibleStatKeys = enabledStatKeys;
const hasContent = showHero || visibleStatKeys.length > 0;
const heroLayoutClass =
heroLayoutBySecondaryStats[String(visibleStatKeys.length > 0) as keyof typeof heroLayoutBySecondaryStats];
const heroRingClass = heroVariantByRing[String(showRing) as keyof typeof heroVariantByRing];
const summaryContent = (
{showHero && (
{t("averageUptime")}
{formatNumber(uptimeValue, 1)}%
{showRing && (
{formatNumber(uptimeValue, 0)}%
}
/>
)}
)}
{!hasContent && (
—
)}
{visibleStatKeys.length > 0 && (
{visibleStatKeys.map((statKey) => {
const Icon = statIcons[statKey];
const color = statColorByKey[statKey];
return (
{formatNumber(combined[statKey], 0)}
{t(`stats.${statKey}`)}
);
})}
)}
);
if (displayMode === "compact") {
return (
{summaryContent}
);
}
const monitorList = (
{monitors
.toSorted((left, right) => statusOrder[left.status] - statusOrder[right.status])
.map((monitor) => (
{monitor.name}
{(isAdvanced || successfulDashboardData.length > 1) && (
{monitor.integrationName}
)}
{t(`status.${monitor.status}`)} ·{" "}
{monitor.uptimePercent24h === null ? "—" : `${formatNumber(monitor.uptimePercent24h, 1)}%`}
))}
);
if (width < 760) {
return (
{summaryContent}
{monitorList}
);
}
return (
{summaryContent}
{monitorList}
);
}
const statusOrder = { down: 0, paused: 1, up: 2 } as const;
function clampPercent(value: number): number {
return Math.min(100, Math.max(0, value));
}
function getUptimeTier(uptime: number): keyof typeof uptimeColorByTier {
const match = uptimeTierByThreshold.find(([min]) => uptime >= min);
const tier = match?.[1] ?? "poor";
return tier;
}
function getUptimeColor(uptime: number): string {
return uptimeColorByTier[getUptimeTier(uptime)];
}
function getGridCols(width: number): number {
const match = gridColsByWidth.find(({ minWidth }) => width >= minWidth);
return match?.cols ?? 2;
}
function getRingSize(width: number): number {
const match = ringSizeByWidth.find(({ minWidth }) => width >= minWidth);
return match?.size ?? 44;
}
function getIconSize(width: number): number {
const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth);
return match?.size ?? 16;
}