Files
AetherForge/server/web/src/components/Charts/HashrateChart.tsx
AetherForge 0be2de81a5
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
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.
2026-06-07 01:08:05 -07:00

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);