diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx index 336ac732b..f7bf8c8aa 100644 --- a/frontend/src/components/ui/progress.tsx +++ b/frontend/src/components/ui/progress.tsx @@ -1,5 +1,5 @@ -import * as React from "react"; import * as ProgressPrimitive from "@radix-ui/react-progress"; +import * as React from "react"; import { cn } from "src/lib/utils"; @@ -23,4 +23,23 @@ const Progress = React.forwardRef< )); Progress.displayName = ProgressPrimitive.Root.displayName; -export { Progress }; +const CircleProgress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + {props.children ||
{`${value || 0}%`}
} +
+)); + +export { CircleProgress, Progress }; diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx index 9e3567304..def7aca2f 100644 --- a/frontend/src/screens/channels/Channels.tsx +++ b/frontend/src/screens/channels/Channels.tsx @@ -7,6 +7,7 @@ import { CopyIcon, ExternalLinkIcon, HandCoins, + Heart, Hotel, InfoIcon, MoreHorizontal, @@ -43,7 +44,7 @@ import { DropdownMenuTrigger, } from "src/components/ui/dropdown-menu.tsx"; import { LoadingButton } from "src/components/ui/loading-button.tsx"; -import { Progress } from "src/components/ui/progress.tsx"; +import { CircleProgress, Progress } from "src/components/ui/progress.tsx"; import { Table, TableBody, @@ -95,6 +96,8 @@ export default function Channels() { const [drainingAlbySharedFunds, setDrainingAlbySharedFunds] = React.useState(false); + const nodeHealth = channels ? getNodeHealth(channels) : 0; + // TODO: move to NWC backend const loadNodeStats = React.useCallback(async () => { if (!channels) { @@ -287,7 +290,7 @@ export default function Channels() { title="Liquidity" description="Manage your lightning node liquidity" contentRight={ - <> +
*/} - + + + + + + + + + Node health: {nodeHealth}% + + + +
} > @@ -797,3 +812,37 @@ export default function Channels() { ); } + +function getNodeHealth(channels: Channel[]) { + const totalChannelCapacitySats = channels + .map((channel) => (channel.localBalance + channel.remoteBalance) / 1000) + .reduce((a, b) => a + b, 0); + + const averageChannelBalance = + channels + .map((channel) => { + const totalBalance = channel.localBalance + channel.remoteBalance; + const expectedBalance = totalBalance / 2; + const actualBalance = + Math.min(channel.localBalance, channel.remoteBalance) / + expectedBalance; + return actualBalance; + }) + .reduce((a, b) => a + b, 0) / (channels.length || 1); + + const numUniqueChannelPartners = new Set( + channels.map((channel) => channel.remotePubkey) + ).size; + + let nodeHealth = Math.ceil( + Math.min(3, numUniqueChannelPartners) * + (100 / 3) * // 3 channels is great + (Math.min(totalChannelCapacitySats, 1_000_000) / 1_000_000) * // 1 million sats or more is great + averageChannelBalance // perfectly balanced is great! + ); + + // above calculation is a bit harsh + nodeHealth = Math.min(nodeHealth * 2, 100); + + return nodeHealth; +}