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
519 changes: 518 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.62.0",
"bitcoinjs-lib": "^7.0.1",
"class-variance-authority": "^0.7.1",
Expand Down
19 changes: 10 additions & 9 deletions src/components/data/HashrateChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { type ReactNode } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Area,
AreaChart,
Expand All @@ -14,6 +15,7 @@ interface HashrateChartProps {
data: { time: string; hashrate: number }[];
title?: string;
description?: string;
info?: ReactNode;
}

/**
Expand Down Expand Up @@ -81,18 +83,18 @@ export function HashrateChart({
data,
title = 'Hashrate History',
description,
info,
}: HashrateChartProps) {
// Don't render chart if no data
if (!data || data.length === 0) {
return (
<Card className="glass-card border-none shadow-sm bg-card/40">
<CardHeader>
<CardTitle className="text-base font-normal text-muted-foreground">
<CardTitle className="text-base font-normal text-muted-foreground flex items-center gap-1.5">
{title}
{info}
</CardTitle>
{description && (
<CardDescription>{description}</CardDescription>
)}
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent>
<div className="h-[200px] w-full flex items-center justify-center text-muted-foreground text-sm">
Expand All @@ -108,12 +110,11 @@ export function HashrateChart({
return (
<Card className="glass-card border-none shadow-sm bg-card/40">
<CardHeader>
<CardTitle className="text-base font-normal text-muted-foreground">
<CardTitle className="text-base font-normal text-muted-foreground flex items-center gap-1.5">
{title}
{info}
</CardTitle>
{description && (
<CardDescription>{description}</CardDescription>
)}
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
{/* pr-4 keeps the right edge of the chart flush with the card padding */}
<CardContent className="pl-2 pr-4">
Expand Down
5 changes: 4 additions & 1 deletion src/components/data/StatCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface StatCardProps {
value: ReactNode;
subtitle?: string;
icon?: ReactNode;
info?: ReactNode;
trend?: {
value: number;
isPositive: boolean;
Expand All @@ -24,14 +25,16 @@ export function StatCard({
value,
subtitle,
icon,
info,
trend,
className,
}: StatCardProps) {
return (
<Card className={cn('glass-card border-none shadow-md bg-card/40', className)}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-1.5">
{title}
{info}
</CardTitle>
{icon && (
<div className="text-muted-foreground">
Expand Down
14 changes: 12 additions & 2 deletions src/components/data/Sv1ClientTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { InfoPopover } from '@/components/ui/info-popover';
import { formatHashrate, truncateHex } from '@/lib/utils';
import { cn } from '@/lib/utils';
import type { Sv1ClientInfo } from '@/types/api';
Expand Down Expand Up @@ -70,7 +71,16 @@ export function Sv1ClientTable({ clients, isLoading, sortKey, sortDir, onSort }:
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => onSort('hashrate')}>
<span className="flex items-center gap-1 hover:text-foreground transition-colors">
Hashrate <SortIcon column="hashrate" sortKey={sortKey} sortDir={sortDir} />
Estimated Hashrate <SortIcon column="hashrate" sortKey={sortKey} sortDir={sortDir} />
<InfoPopover>
Please note that this is a statistical estimation, as there's no way for a proxy
to know the real hashrate of each worker. The proxy expects share submission to
happen at some specific rate, based on the minimal worker hashrate provided at
startup. As time goes on, it estimates the hashrate (and adjusts difficulty
targets) according to the share submission rate of each worker. If some specific
worker hashrate diverges too much from the minimal worker hashrate, it might
take a while for this estimation to converge to the real hashrate.
</InfoPopover>
</span>
</TableHead>
<TableHead className="hidden md:table-cell">Channel</TableHead>
Expand All @@ -97,7 +107,7 @@ export function Sv1ClientTable({ clients, isLoading, sortKey, sortDir, onSort }:
{client.user_identity || '-'}
</TableCell>
<TableCell className="font-mono">
{client.hashrate !== null ? formatHashrate(client.hashrate) : '-'}
{client.hashrate !== null ? `~${formatHashrate(client.hashrate)}` : '-'}
</TableCell>
<TableCell className="hidden md:table-cell font-mono text-xs text-muted-foreground">
{client.channel_id !== null ? client.channel_id : '-'}
Expand Down
17 changes: 15 additions & 2 deletions src/components/data/Sv2ClientTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { InfoPopover } from '@/components/ui/info-popover';
import { formatHashrate } from '@/lib/utils';
import { cn } from '@/lib/utils';
import type { ClientMetadata } from '@/types/api';
Expand Down Expand Up @@ -47,7 +48,19 @@ export function Sv2ClientTable({ clients, isLoading, onClientClick }: Sv2ClientT
<TableRow className="hover:bg-transparent border-border/40">
<TableHead className="w-[80px]">Client ID</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Hashrate</TableHead>
<TableHead className="text-right">
<span className="flex items-center justify-end gap-1.5">
Estimated Hashrate
<InfoPopover>
Your proxy cannot directly measure how fast your miner is hashing. It estimates
hashrate indirectly: it knows the difficulty of the work it assigned you, and it
counts the valid shares you submit. From those two values it calculates how much
hashing you must be doing. This is your estimated hashrate. Sampled every 5
seconds. May take up to 60 seconds to reflect your miner's actual output after
connecting.
</InfoPopover>
</span>
</TableHead>
<TableHead className="text-right hidden md:table-cell">Extended Channels</TableHead>
<TableHead className="text-right hidden lg:table-cell">Standard Channels</TableHead>
<TableHead className="text-right">Total Channels</TableHead>
Expand Down Expand Up @@ -82,7 +95,7 @@ export function Sv2ClientTable({ clients, isLoading, onClientClick }: Sv2ClientT
</div>
</TableCell>
<TableCell className="text-right font-mono font-medium">
{formatHashrate(client.total_hashrate)}
~{formatHashrate(client.total_hashrate)}
</TableCell>
<TableCell className="text-right font-mono hidden md:table-cell text-muted-foreground">
{client.extended_channels_count}
Expand Down
42 changes: 42 additions & 0 deletions src/components/ui/info-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { Info } from 'lucide-react';
import * as Popover from '@radix-ui/react-popover';
import { cn } from '@/lib/utils';

interface InfoPopoverProps {
children: React.ReactNode;
}

export function InfoPopover({ children }: InfoPopoverProps) {
const [open, setOpen] = useState(false);

return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<button
className="inline-flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
aria-label="More information"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onClick={() => setOpen(prev => !prev)}
>
<Info className="h-3.5 w-3.5" />
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
sideOffset={6}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
className={cn(
'z-50 w-72 rounded-xl border border-border/40 bg-card px-3 py-2 text-sm text-muted-foreground shadow-md',
'animate-in fade-in-0 zoom-in-95',
'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
)}
>
{children}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
15 changes: 13 additions & 2 deletions src/pages/UnifiedDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useMemo } from 'react';
import { AlertTriangle, Search, Play } from 'lucide-react';
import { InfoPopover } from '@/components/ui/info-popover';
import { MinerConnectionInfo } from '@/components/setup/MinerConnectionInfo';
import { Shell } from '@/components/layout/Shell';
import { StatCard } from '@/components/data/StatCard';
Expand Down Expand Up @@ -228,9 +229,14 @@ export function UnifiedDashboard() {
{/* Hero Stats Section */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Total Hashrate"
title="Total Estimated Hashrate"
value={formatHashrate(totalHashrate)}
subtitle={`${totalClientChannels} client channel(s)`}
info={
<InfoPopover>
Estimated hashrate sampled every 5 seconds. May take a few minutes to reflect your miner's actual output.
</InfoPopover>
}
/>

<StatCard
Expand Down Expand Up @@ -302,10 +308,15 @@ export function UnifiedDashboard() {
</div>

{/* Main Chart - Real data accumulated over time */}
<HashrateChart
<HashrateChart
data={hashrateHistory}
title="Hashrate History"
description="Real-time data collected since page load"
info={
<InfoPopover>
Estimated hashrate sampled every 5 seconds. May take a few minutes to reflect your miner's actual output.
</InfoPopover>
}
/>

{/* Loading State */}
Expand Down
Loading