import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { FaCircleCheck } from "react-icons/fa6"; import { LuRefreshCw } from "react-icons/lu"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import useSWR from "swr"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import TimeAgo from "@/components/dynamic/TimeAgo"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { ConnectionQualityIndicator } from "@/components/camera/ConnectionQualityIndicator"; import HardwareStatusRow from "@/components/health/HardwareStatusRow"; import { useHardwareHealth } from "@/hooks/use-hardware-health"; import { useHealthChecks } from "@/hooks/use-health-checks"; import type { FrigateConfig } from "@/types/frigateConfig"; import type { HardwareRow } from "@/utils/health"; import { streamHealth } from "@/utils/streamHealth"; function Card({ title, className, action, subtitle, children, }: { title: string; className?: string; /** an icon button right after the title */ action?: React.ReactNode; /** a muted line under the title */ subtitle?: React.ReactNode; children: React.ReactNode; }) { return (
{title} {action}
{subtitle && (
{subtitle}
)}
{children}
); } function InlineAction({ label, busy, disabled, onClick, }: { label: string; busy?: boolean; disabled?: boolean; onClick: () => void; }) { return ( {label} ); } function Group({ title, rows }: { title: string; rows: HardwareRow[] }) { const { t } = useTranslation(["views/system"]); return ( {rows.length === 0 ? (
{t("health.hardware.nothingConfigured")}
) : (
{rows.map((row) => ( ))}
)}
); } /** Stream checks live here: the check sits next to its result. */ function StreamsGroup() { const { t } = useTranslation(["views/system", "views/settings"]); const { data: config } = useSWR("config", { revalidateOnFocus: false, }); const { stream } = useHealthChecks(); const summary = useMemo( () => (config ? streamHealth(config, stream.results, t) : undefined), [config, stream.results, t], ); const row: HardwareRow | undefined = useMemo(() => { if (stream.running || !summary || !stream.results) { return undefined; } const flagged = summary.checked - summary.clean; return { id: "streams", state: flagged > 0 ? "warning" : "ok", label: t("health.hardware.streamsChecked", { count: summary.checked }), detail: flagged > 0 ? t("health.hardware.streamsFlagged", { count: flagged }) : t("health.hardware.streamsClean"), }; }, [stream.running, stream.results, summary, t]); return ( stream.run()} /> } subtitle={ stream.running ? t("health.hardware.streamsChecking", { done: stream.total - stream.pending.length, total: stream.total, }) : stream.results && ( <> {t("health.hardware.checked")}{" "} ) } >
{row ? ( ) : ( )}
); } function HardwareHeading() { const { t } = useTranslation(["views/system"]); const { hardware } = useHealthChecks(); return (
{t("health.hardware.title")}
hardware.recheck()} />
{hardware.rechecking ? (
{t("health.hardware.probing")}
) : ( hardware.probedAt && (
{t("health.hardware.probed")}{" "}
) )}
); } export default function HardwarePane() { const { t } = useTranslation(["views/system"]); const { rows, statsLoaded } = useHardwareHealth(); return (
{!rows ? ( ) : ( <>
{!statsLoaded ? ( ) : rows.cameras.length === 0 ? (
{t("health.hardware.allCamerasExcellent")}
) : (
{rows.cameras.map((cell) => (
{t("health.hardware.fps", { camera: cell.cameraFps.toFixed(1), expected: cell.expectedFps, })}
))}
)}
)}
); }