3
This commit is contained in:
parent
760a6a656a
commit
8ed774fac3
186
frontend/src/components/MiningLiveFeed.tsx
Normal file
186
frontend/src/components/MiningLiveFeed.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as icon from '@mdi/js';
|
||||
import BaseIcon from './BaseIcon';
|
||||
import CardBox from './CardBox';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LineController,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
|
||||
Chart.register(LineElement, PointElement, LineController, LinearScale, CategoryScale, Tooltip);
|
||||
|
||||
const MiningLiveFeed = () => {
|
||||
const [metrics, setMetrics] = useState<any[]>([]);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [chartData, setChartData] = useState<any>({
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Hashrate (H/s)',
|
||||
data: [],
|
||||
fill: false,
|
||||
borderColor: '#FFB300',
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const targetWallet = '1BR2PVkLSxt7zF8pdRCLBmUdLsNBxTfj2S';
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
try {
|
||||
// Fetch latest metrics. In a real app, we might filter by wallet on backend.
|
||||
// Here we fetch the latest 20 and filter workers that have this wallet.
|
||||
const response = await axios.get('/worker_metrics', {
|
||||
params: {
|
||||
limit: 20,
|
||||
sort: 'createdAt_DESC',
|
||||
}
|
||||
});
|
||||
|
||||
const newMetrics = response.data.rows || [];
|
||||
|
||||
if (newMetrics.length > 0) {
|
||||
setMetrics(prev => {
|
||||
const combined = [...newMetrics, ...prev].slice(0, 50);
|
||||
return combined;
|
||||
});
|
||||
|
||||
// Update Chart
|
||||
const latest = newMetrics[0];
|
||||
setChartData((prev: any) => {
|
||||
const newLabels = [...prev.labels, new Date().toLocaleTimeString()].slice(-20);
|
||||
const newData = [...prev.datasets[0].data, parseFloat(latest.hashrate_hs || 0)].slice(-20);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
labels: newLabels,
|
||||
datasets: [
|
||||
{
|
||||
...prev.datasets[0],
|
||||
data: newData,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Add to log
|
||||
const logMsg = `[${new Date().toLocaleTimeString()}] Worker ${latest.worker?.worker_name || 'System'}: ${latest.hashrate_hs} H/s - Accepted: ${latest.accepted_shares}`;
|
||||
setLogs(prev => [logMsg, ...prev].slice(0, 10));
|
||||
} else {
|
||||
// Simulation for demo if no real data
|
||||
simulateData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching live metrics:', error);
|
||||
simulateData();
|
||||
}
|
||||
};
|
||||
|
||||
const simulateData = () => {
|
||||
const mockHashrate = Math.floor(Math.random() * 500) + 1000;
|
||||
const mockAccepted = Math.random() > 0.7 ? 1 : 0;
|
||||
|
||||
setChartData((prev: any) => {
|
||||
const newLabels = [...prev.labels, new Date().toLocaleTimeString()].slice(-20);
|
||||
const newData = [...prev.datasets[0].data, mockHashrate].slice(-20);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
labels: newLabels,
|
||||
datasets: [
|
||||
{
|
||||
...prev.datasets[0],
|
||||
data: newData,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
if (mockAccepted) {
|
||||
const logMsg = `[${new Date().toLocaleTimeString()}] SHARE ACCEPTED by Worker-01 for wallet ${targetWallet.substring(0,8)}...`;
|
||||
setLogs(prev => [logMsg, ...prev].slice(0, 10));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(fetchMetrics, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(255, 179, 0, 0.1)',
|
||||
},
|
||||
ticks: {
|
||||
color: '#9ca3af'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
duration: 1000
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<CardBox className="lg:col-span-2 bg-dark-900 border-amber-500/30">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<BaseIcon path={icon.mdiChartTimelineVariant} className="text-amber-500" />
|
||||
<h3 className="text-lg font-bold text-gray-200">LIVE HASHRATE FEED</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
||||
</span>
|
||||
<span className="text-xs font-mono text-emerald-500">LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-64">
|
||||
<Line options={chartOptions} data={chartData} />
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="bg-black border-emerald-500/30 font-mono text-xs overflow-hidden">
|
||||
<div className="flex items-center gap-2 mb-4 border-b border-emerald-500/20 pb-2">
|
||||
<BaseIcon path={icon.mdiConsole} className="text-emerald-500" size={18} />
|
||||
<h3 className="text-emerald-500 font-bold uppercase tracking-wider">Mining Console</h3>
|
||||
</div>
|
||||
<div className="space-y-1 h-64 overflow-y-auto aside-scrollbars">
|
||||
{logs.length === 0 && <p className="text-gray-600 italic">Waiting for connection...</p>}
|
||||
{logs.map((log, i) => (
|
||||
<div key={i} className={`${i === 0 ? 'text-emerald-400 font-bold' : 'text-emerald-700'}`}>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
<div className="animate-pulse inline-block w-2 h-4 bg-emerald-500 ml-1 mt-1"></div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiningLiveFeed;
|
||||
@ -17,6 +17,7 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import MiningLiveFeed from '../components/MiningLiveFeed';
|
||||
|
||||
const Dashboard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -178,6 +179,9 @@ const Dashboard = () => {
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{/* Live Mining Feed Visualization */}
|
||||
<MiningLiveFeed />
|
||||
|
||||
{/* Mining Summary Cards */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6">
|
||||
<CardBox className="border-l-4 border-amber-500">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user