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
63 changes: 21 additions & 42 deletions src/components/data/DownstreamWorkerTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,40 +198,16 @@ export function DownstreamWorkerTable({
<Table className="min-w-[1680px]">
<TableHeader className="bg-muted/30">
<TableRow className="hover:bg-transparent">
<TableHead className="w-[132px] cursor-pointer select-none whitespace-nowrap" onClick={() => onSort('connection_id')}>
<TableHead className="cursor-pointer select-none whitespace-nowrap" onClick={() => onSort('channel_type')}>
<span className="flex items-center gap-1 whitespace-nowrap hover:text-foreground transition-colors">
Connection Id
<SortIcon column="connection_id" sortKey={sortKey} sortDir={sortDir} />
<InfoPopover>
Worker channels with the same connection ID belong to the same downstream
connection.
</InfoPopover>
</span>
</TableHead>
<TableHead className="w-[120px] cursor-pointer select-none whitespace-nowrap" onClick={() => onSort('channel_id')}>
<span className="flex items-center gap-1 whitespace-nowrap hover:text-foreground transition-colors">
Channel Id <SortIcon column="channel_id" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="w-[220px] cursor-pointer select-none whitespace-nowrap" onClick={() => onSort('channel_type')}>
<span className="flex items-center gap-1 whitespace-nowrap hover:text-foreground transition-colors">
Channel Type <SortIcon column="channel_type" sortKey={sortKey} sortDir={sortDir} />
Connection Type <SortIcon column="channel_type" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="cursor-pointer select-none" onClick={() => onSort('identity')}>
<span className="flex items-center gap-1 hover:text-foreground transition-colors">
Username / Identity <SortIcon column="identity" sortKey={sortKey} sortDir={sortDir} />
</span>
</TableHead>
<TableHead className="whitespace-nowrap">Management IP</TableHead>
<TableHead className="whitespace-nowrap">Telemetry</TableHead>
<TableHead className="whitespace-nowrap">Miner</TableHead>
<TableHead className="whitespace-nowrap">Firmware</TableHead>
<TableHead className="whitespace-nowrap">State</TableHead>
<TableHead className="text-right whitespace-nowrap">Temp</TableHead>
<TableHead className="text-right whitespace-nowrap">Power</TableHead>
<TableHead className="text-right whitespace-nowrap">Efficiency</TableHead>
<TableHead className="text-right whitespace-nowrap">Uptime</TableHead>
<TableHead className="text-right cursor-pointer select-none" onClick={() => onSort('estimated_hashrate')}>
<span className="flex items-center justify-end gap-1 hover:text-foreground transition-colors">
Hashrate
Expand All @@ -252,17 +228,20 @@ export function DownstreamWorkerTable({
</span>
</TableHead>
)}
<TableHead className="whitespace-nowrap">Telemetry</TableHead>
<TableHead className="whitespace-nowrap">Management IP</TableHead>
<TableHead className="whitespace-nowrap">Miner</TableHead>
<TableHead className="whitespace-nowrap">Firmware</TableHead>
<TableHead className="whitespace-nowrap">State</TableHead>
<TableHead className="text-right whitespace-nowrap">Temp</TableHead>
<TableHead className="text-right whitespace-nowrap">Power</TableHead>
<TableHead className="text-right whitespace-nowrap">Efficiency</TableHead>
<TableHead className="text-right whitespace-nowrap">Uptime</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workers.map((worker) => (
<TableRow key={`${worker.connection_id}-${worker.channel_type}-${worker.channel_id ?? 'na'}-${worker.identity}`} className="hover:bg-muted/20 group">
<TableCell className="font-mono text-xs text-muted-foreground">
{worker.connection_id}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground">
{worker.channel_id ?? '-'}
</TableCell>
<TableCell>
<span
className={cn(
Expand All @@ -276,9 +255,14 @@ export function DownstreamWorkerTable({
<TableCell className="text-muted-foreground">
{worker.identity || '-'}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground whitespace-nowrap">
{worker.management_ip || '-'}
<TableCell className="text-right font-mono font-medium whitespace-nowrap">
{formatWorkerHashrate(worker)}
</TableCell>
{showBestDiff && (
<TableCell className="text-right font-mono text-muted-foreground whitespace-nowrap">
{worker.best_diff !== null && worker.best_diff > 0 ? formatDifficulty(worker.best_diff) : '-'}
</TableCell>
)}
<TableCell
className={cn(
'text-xs whitespace-nowrap',
Expand All @@ -287,6 +271,9 @@ export function DownstreamWorkerTable({
>
{getTelemetryStatusLabel(worker.miner_telemetry_status) || '-'}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground whitespace-nowrap">
{worker.management_ip || '-'}
</TableCell>
<TableCell className="text-muted-foreground whitespace-nowrap">
{getMinerLabel(worker.miner_telemetry)}
</TableCell>
Expand All @@ -313,14 +300,6 @@ export function DownstreamWorkerTable({
<TableCell className="text-right font-mono text-muted-foreground whitespace-nowrap">
{getUptimeLabel(worker.miner_telemetry)}
</TableCell>
<TableCell className="text-right font-mono font-medium whitespace-nowrap">
{formatWorkerHashrate(worker)}
</TableCell>
{showBestDiff && (
<TableCell className="text-right font-mono text-muted-foreground whitespace-nowrap">
{worker.best_diff !== null && worker.best_diff > 0 ? formatDifficulty(worker.best_diff) : '-'}
</TableCell>
)}
</TableRow>
))}
</TableBody>
Expand Down
37 changes: 25 additions & 12 deletions src/components/setup/MinerConnectionInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import { Copy, Check } from 'lucide-react';
import { TRANSLATOR_PORT, JDC_PORT, JDC_AUTHORITY_PUBLIC_KEY } from '@/lib/ports';
import { useHostEnv } from '@/hooks/useHostEnv';
import { InfoPopover } from '@/components/ui/info-popover';

function copyWithSelectionFallback(text: string): boolean {
const textArea = document.createElement('textarea');
Expand Down Expand Up @@ -82,6 +83,22 @@ function CopyableAddress({ address }: { address: string }) {
);
}

function HostHint() {
return (
<InfoPopover ariaLabel="Local network IP help">
Replace{' '}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-foreground">
&lt;your-machine-ip&gt;
</code>{' '}
with your local network IP (for example,{' '}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-foreground">
192.168.1.100
</code>
).
</InfoPopover>
);
}

interface MinerConnectionInfoProps {
isJdMode: boolean;
centered?: boolean;
Expand All @@ -93,12 +110,6 @@ export function MinerConnectionInfo({ isJdMode, centered = false }: MinerConnect
const translatorUrl = `stratum+tcp://${host}:${TRANSLATOR_PORT}`;
const jdcUrl = `stratum2+tcp://${host}:${JDC_PORT}/${JDC_AUTHORITY_PUBLIC_KEY}`;

const hint = (
<p className="text-xs text-muted-foreground">
Replace <code className="font-mono bg-muted px-1 py-0.5 rounded text-foreground">&lt;your-machine-ip&gt;</code> with your local network IP (e.g. <code className="font-mono bg-muted px-1 py-0.5 rounded text-foreground">192.168.1.100</code>).
</p>
);

// When only one card is shown (SV1-only mode) it should stretch to full width.
// In JD mode two cards sit side-by-side on md+ screens.
const wrapperClass = centered
Expand All @@ -110,18 +121,20 @@ export function MinerConnectionInfo({ isJdMode, centered = false }: MinerConnect
return (
<div className={wrapperClass}>
<div className={`p-4 rounded-xl border border-border bg-card space-y-2${centered ? ' w-full max-w-sm' : ''}`}>
<div className="font-semibold text-sm">SV1 Firmware</div>
<div className="text-xs text-muted-foreground">Point to the Translator Proxy</div>
<div className="flex items-center gap-1.5">
<div className="font-semibold text-sm">SV1 Firmware</div>
{!stratumHost && <HostHint />}
</div>
<CopyableAddress address={translatorUrl} />
{!stratumHost && hint}
</div>

{isJdMode && (
<div className={`p-4 rounded-xl border border-border bg-card space-y-2${centered ? ' w-full max-w-sm' : ''}`}>
<div className="font-semibold text-sm">SV2 Firmware</div>
<div className="text-xs text-muted-foreground">Point directly to the JD Client</div>
<div className="flex items-center gap-1.5">
<div className="font-semibold text-sm">SV2 Firmware</div>
{!stratumHost && <HostHint />}
</div>
<CopyableAddress address={jdcUrl} />
{!stratumHost && hint}
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useConnectionStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function useConnectionStatus(): ConnectionStatus {

return {
status,
statusLabel: isSovereignSolo ? 'Mining services healthy' : null,
statusLabel: isSovereignSolo ? 'Sovereign Solo' : null,
poolName: isPoolConnected ? (poolName ?? null) : null,
uptime: isPoolConnected ? (poolGlobal?.uptime_secs ?? 0) : 0,
};
Expand Down
98 changes: 39 additions & 59 deletions src/pages/UnifiedDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import {
usePoolData,
useSv1ClientsData,
useTranslatorServerChannels,
useTranslatorHealth,
useJdcHealth,
} from '@/hooks/usePoolData';
Expand Down Expand Up @@ -94,6 +95,8 @@ export function UnifiedDashboard() {
isLoading: poolLoading,
isError: poolError,
} = usePoolData(templateMode);
const { data: translatorServerChannels } = useTranslatorServerChannels(isJdMode);
const sv1ServerChannels = isJdMode ? translatorServerChannels : serverChannels;
const isAggregatedTproxy = !isJdMode && isAggregatedTproxyPoolName(configPoolName);

// SV1 clients (always from Translator)
Expand Down Expand Up @@ -248,14 +251,6 @@ export function UnifiedDashboard() {
const directSv2WorkerCount = downstreamWorkers.length;
const jdWorkerCount = directSv2WorkerCount + sv1TotalClients;
const jdActiveWorkerCount = directSv2WorkerCount + sv1ActiveCount;
const jdClientChannelCount = directSv2WorkerCount + sv1ActiveCount;
const userFacingDownstreamConnectionCount = useMemo(() => {
if (!isJdMode) {
return 0;
}

return new Set(downstreamWorkers.map((worker) => worker.connection_id)).size + sv1TotalClients;
}, [downstreamWorkers, isJdMode, sv1TotalClients]);
const totalWorkers = isJdMode ? jdWorkerCount : sv1TotalClients;
const activeWorkers = isJdMode ? jdActiveWorkerCount : sv1ActiveCount;
const workerTableLoading = isJdMode ? (isSv2ClientsLoading || sv1Loading) : sv1Loading;
Expand All @@ -269,10 +264,6 @@ export function UnifiedDashboard() {
: (poolGlobal?.sv2_clients?.total_hashrate ?? 0))
: (sv1Data ? sv1TotalHashrate : (poolGlobal?.sv1_clients?.total_hashrate ?? 0));

const totalClientChannels = isJdMode
? jdClientChannelCount
: sv1ActiveCount;

// Scope hashrate history to the active pool + mode so stale samples from a
// previous configuration are never shown after a reconfigure.
// Falls back to 'default' while setup status is still loading or in
Expand Down Expand Up @@ -322,10 +313,21 @@ export function UnifiedDashboard() {
}, [directSv2Clients, isJdMode, serverChannels, sv2Clients]);

const bestDiffEntries = useMemo(() => {
if (isJdMode) {
if (!sv2Clients) return [];
const sv1BestDiffEntries = sv1ServerChannels ? [
...sv1ServerChannels.extended_channels.map((channel) => ({
key: `translator:server:extended:${channel.channel_id}:${channel.user_identity}`,
value: channel.best_diff,
})),
...sv1ServerChannels.standard_channels.map((channel) => ({
key: `translator:server:standard:${channel.channel_id}:${channel.user_identity}`,
value: channel.best_diff,
})),
] : [];

return directSv2Clients.flatMap((client) => [
if (!isJdMode || !sv2Clients) return sv1BestDiffEntries;

return [
...directSv2Clients.flatMap((client) => [
...client.extended_channels.map((channel) => ({
key: `jdc:${client.client_id}:extended:${channel.channel_id}:${channel.user_identity}`,
value: channel.best_diff,
Expand All @@ -334,22 +336,23 @@ export function UnifiedDashboard() {
key: `jdc:${client.client_id}:standard:${channel.channel_id}:${channel.user_identity}`,
value: channel.best_diff,
})),
]);
}
]),
...sv1BestDiffEntries,
];
}, [directSv2Clients, isJdMode, sv1ServerChannels, sv2Clients]);

if (!serverChannels) return [];
const sv1BestDiffByChannelId = useMemo(() => {
const bestDiffByChannelId = new Map<number, number>();

return [
...serverChannels.extended_channels.map((channel) => ({
key: `translator:server:extended:${channel.channel_id}:${channel.user_identity}`,
value: channel.best_diff,
})),
...serverChannels.standard_channels.map((channel) => ({
key: `translator:server:standard:${channel.channel_id}:${channel.user_identity}`,
value: channel.best_diff,
})),
];
}, [directSv2Clients, isJdMode, serverChannels, sv2Clients]);
if (!sv1ServerChannels) return bestDiffByChannelId;

[...sv1ServerChannels.extended_channels, ...sv1ServerChannels.standard_channels].forEach((channel) => {
const currentBestDiff = bestDiffByChannelId.get(channel.channel_id) ?? 0;
bestDiffByChannelId.set(channel.channel_id, Math.max(currentBestDiff, channel.best_diff));
});

return bestDiffByChannelId;
}, [sv1ServerChannels]);

const shareStatsEntries = useMemo(() => {
if (!serverChannels) return [];
Expand All @@ -376,18 +379,9 @@ export function UnifiedDashboard() {
const bestDiff = usePersistentBestDifficulty(bestDiffEntries, historyConfigKey);
const shareStats = usePersistentShareStats(shareStatsEntries, historyConfigKey);

// Number of upstream pool channels (for shares subtitle)
const poolChannelCount = (serverChannels?.total_extended || 0) + (serverChannels?.total_standard || 0);

// Number of client channels (for best diff subtitle)
const clientChannelCount = isJdMode
? jdClientChannelCount
: sv1ActiveCount;
const bestDiffSubtitle = clientChannelCount > 0
? `from ${clientChannelCount} client channel(s)`
: undefined;

const hasBestDiffSource = isJdMode ? !!sv2Clients : !!serverChannels;
const hasBestDiffSource = isJdMode
? !!sv2Clients || !!sv1ServerChannels
: !!sv1ServerChannels;

useEffect(() => {
if (isAggregatedTproxy && sortKey === 'best_diff') {
Expand All @@ -412,7 +406,9 @@ export function UnifiedDashboard() {
miner_telemetry: client.miner_telemetry,
estimated_hashrate: resolvedHashrate.hashrate,
hashrate_source: resolvedHashrate.source,
best_diff: null,
best_diff: client.channel_id === null || client.channel_id === undefined
? null
: (sv1BestDiffByChannelId.get(client.channel_id) ?? null),
search_text: [
client.sv1_username,
client.sv1_worker_name,
Expand All @@ -427,7 +423,7 @@ export function UnifiedDashboard() {
].join(' ').toLowerCase(),
};
});
}, [allSv1Clients]);
}, [allSv1Clients, sv1BestDiffByChannelId]);

const dashboardWorkers = useMemo<DashboardWorkerRow[]>(() => {
if (isJdMode) {
Expand Down Expand Up @@ -580,7 +576,6 @@ export function UnifiedDashboard() {
<StatCard
title="Total Hashrate"
value={formatHashrate(totalHashrate)}
subtitle={`${totalClientChannels} client channel(s)`}
info={
<InfoPopover>
Uses miner-reported telemetry when available. Otherwise falls back to the proxy's
Expand All @@ -600,11 +595,6 @@ export function UnifiedDashboard() {
</span>
)
}
subtitle={
isJdMode
? `${userFacingDownstreamConnectionCount} downstream connection(s)`
: `${totalWorkers - activeWorkers} offline workers`
}
/>

<StatCard
Expand Down Expand Up @@ -689,22 +679,12 @@ export function UnifiedDashboard() {

return <span className={colorClass}>{label}</span>;
})()}
subtitle={(() => {
const { submitted, rejected } = shareStats;
if (submitted === 0) return `via ${poolChannelCount} channel(s)`;
const rejectionRate = (rejected / submitted) * 100;
const rejRateLabel = rejected === 0
? '0%'
: `${Math.max(rejectionRate, 0.01).toFixed(2)}%`;
return `${submitted.toLocaleString()} submitted · ${rejected.toLocaleString()} rejected (${rejRateLabel})`;
})()}
/>
)}

<StatCard
title="Best Difficulty"
value={hasBestDiffSource ? formatDifficulty(bestDiff) : '-'}
subtitle={bestDiffSubtitle}
/>
</div>

Expand Down
Loading