Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
193 lines
5.8 KiB
TypeScript
193 lines
5.8 KiB
TypeScript
import { memo } from 'react';
|
|
import {
|
|
Area,
|
|
AreaChart,
|
|
CartesianGrid,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
import {
|
|
chartSeriesDelta,
|
|
chartSeriesPeak,
|
|
type ChartDisplayMode,
|
|
} from '../../help/chartSampleData';
|
|
import './HashrateChart.css';
|
|
|
|
export interface ChartPoint {
|
|
time: string;
|
|
value: number;
|
|
label?: string;
|
|
}
|
|
|
|
interface HashrateChartProps {
|
|
data: ChartPoint[];
|
|
title?: string;
|
|
color?: string;
|
|
unit?: string;
|
|
height?: number;
|
|
displayMode?: ChartDisplayMode;
|
|
}
|
|
|
|
const GRAD_IDS = ['cyan', 'magenta', 'amber', 'green', 'purple'] as const;
|
|
|
|
function colorToId(color: string): string {
|
|
if (color.includes('e8f5') || color.includes('f5ff') || color.includes('06b6d4') || color === '#00e8f5' || color === '#00f5ff' || color.includes('neon-cyan')) return 'cyan';
|
|
if (color.includes('2da6') || color.includes('8b5cf6')) return 'magenta';
|
|
if (color.includes('b020') || color.includes('eab308')) return 'amber';
|
|
if (color.includes('39ff') || color.includes('22c55e')) return 'green';
|
|
return 'purple';
|
|
}
|
|
|
|
function HashrateChart({
|
|
data,
|
|
title,
|
|
color = 'var(--neon-cyan)',
|
|
unit = 'H/s',
|
|
height = 280,
|
|
displayMode = 'live',
|
|
}: HashrateChartProps) {
|
|
const gradId = colorToId(color);
|
|
const peak = chartSeriesPeak(data);
|
|
const delta = chartSeriesDelta(data);
|
|
const liveLabel = displayMode === 'live' ? '● LIVE' : '○ IDLE';
|
|
const liveClass = displayMode === 'live' ? 'pulse' : 'empty';
|
|
|
|
if (data.length === 0) {
|
|
return (
|
|
<div className="chart-empty neon-chart-panel wealth-empty">
|
|
<div className="chart-empty-icon">◈</div>
|
|
<p className="font-tech">{title || 'Telemetry'}</p>
|
|
<span>No live data yet — connect miners to populate this chart</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="chart-wrap neon-chart-panel wealth-chart">
|
|
{title && (
|
|
<div className="chart-header">
|
|
<h3 className="chart-title font-display">{title}</h3>
|
|
<div className="chart-header-meta">
|
|
<span className="chart-peak font-tech">
|
|
PEAK {formatFull(peak, unit)}
|
|
</span>
|
|
{delta != null && (
|
|
<span className={`chart-delta font-tech ${delta >= 0 ? 'up' : 'down'}`}>
|
|
{delta >= 0 ? '▲' : '▼'} {Math.abs(delta).toFixed(1)}%
|
|
</span>
|
|
)}
|
|
<span className={`chart-live ${liveClass}`}>{liveLabel}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<ResponsiveContainer width="100%" height={height}>
|
|
<AreaChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id={`area-fill-${gradId}`} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor={color} stopOpacity={0.55} />
|
|
<stop offset="50%" stopColor={color} stopOpacity={0.15} />
|
|
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
</linearGradient>
|
|
<filter id={`glow-${gradId}`}>
|
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
<feMerge>
|
|
<feMergeNode in="blur" />
|
|
<feMergeNode in="SourceGraphic" />
|
|
</feMerge>
|
|
</filter>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="4 8" stroke="rgba(201, 162, 39, 0.12)" vertical={false} />
|
|
<XAxis
|
|
dataKey="time"
|
|
stroke="rgba(196, 181, 160, 0.5)"
|
|
fontSize={10}
|
|
fontFamily="Orbitron, monospace"
|
|
tickLine={false}
|
|
axisLine={{ stroke: 'rgba(201, 162, 39, 0.25)' }}
|
|
/>
|
|
<YAxis
|
|
stroke="rgba(196, 181, 160, 0.5)"
|
|
fontSize={10}
|
|
fontFamily="Orbitron, monospace"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(v) => formatShort(v, unit)}
|
|
/>
|
|
<Tooltip
|
|
content={(props) => (
|
|
<NeonTooltip
|
|
active={props.active}
|
|
payload={props.payload as { value: number }[] | undefined}
|
|
label={props.label as string | undefined}
|
|
unit={unit}
|
|
title={title}
|
|
color={color}
|
|
/>
|
|
)}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="value"
|
|
stroke={color}
|
|
strokeWidth={2.5}
|
|
fill={`url(#area-fill-${gradId})`}
|
|
filter={`url(#glow-${gradId})`}
|
|
animationDuration={800}
|
|
animationEasing="ease-out"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NeonTooltip({
|
|
active,
|
|
payload,
|
|
label,
|
|
unit,
|
|
title,
|
|
color,
|
|
}: {
|
|
active?: boolean;
|
|
payload?: { value: number }[];
|
|
label?: string;
|
|
unit: string;
|
|
title?: string;
|
|
color: string;
|
|
}) {
|
|
if (!active || !payload?.length) return null;
|
|
const v = payload[0].value;
|
|
return (
|
|
<div className="neon-tooltip">
|
|
<div className="neon-tooltip-time font-tech">{label}</div>
|
|
<div className="neon-tooltip-value" style={{ color, textShadow: `0 0 12px ${color}` }}>
|
|
{formatFull(v, unit)}
|
|
</div>
|
|
<div className="neon-tooltip-label">{title || 'Reading'}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatShort(v: number, unit: string): string {
|
|
if (unit === 'H/s') {
|
|
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
|
|
return `${v.toFixed(0)}`;
|
|
}
|
|
return `${v.toFixed(0)}${unit === '%' ? '%' : ''}`;
|
|
}
|
|
|
|
function formatFull(v: number, unit: string): string {
|
|
if (unit === 'H/s') {
|
|
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(3)} MH/s`;
|
|
if (v >= 1_000) return `${(v / 1_000).toFixed(2)} KH/s`;
|
|
return `${v.toFixed(0)} H/s`;
|
|
}
|
|
return `${v.toFixed(1)}${unit}`;
|
|
}
|
|
|
|
export default memo(HashrateChart);
|