Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.
Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
@@ -1,23 +1,58 @@
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { api } from '../api/client';
|
||||
import { useState, useEffect, useMemo, type CSSProperties } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { Share } from '../types';
|
||||
import HashrateChart from '../components/Charts/HashrateChart';
|
||||
import GaugeRing from '../components/Charts/GaugeRing';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import { FleetPipelineStatus, ActivityPulse } from '../components/Visual/VisualComponents';
|
||||
import { AlertBanner, PoolStatusPanel, AIActivityPanel, EarningsEstimator } from '../components/Fleet/FleetPanels';
|
||||
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
|
||||
import '../components/Fleet/AgentRemoteActions.css';
|
||||
import './Pages.css';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { isConnected, agents } = useWebSocket();
|
||||
const { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity } = useWebSocket();
|
||||
const [shares, setShares] = useState<Share[]>([]);
|
||||
const [restAlerts, setRestAlerts] = useState<typeof fleetAlerts>([]);
|
||||
const [restPools, setRestPools] = useState<typeof poolStatus>([]);
|
||||
const [restAI, setRestAI] = useState<typeof aiActivity>([]);
|
||||
const [subtitle, setSubtitle] = useState('security is just an emotion');
|
||||
const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
const [memHistory, setMemHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
const [hasBuilds, setHasBuilds] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.getRecentShares(20).then(setShares).catch(console.error);
|
||||
api.listBuilds().then((b) => setHasBuilds(b.length > 0)).catch(console.error);
|
||||
api.getConfig()
|
||||
.then((cfg) => {
|
||||
const s = cfg.server?.dashboard_subtitle?.trim();
|
||||
if (s) setSubtitle(s);
|
||||
})
|
||||
.catch(console.error);
|
||||
api.getAlerts().then(setRestAlerts).catch(console.error);
|
||||
api.getPoolStatus().then(setRestPools).catch(console.error);
|
||||
api.getAIActivity().then(setRestAI).catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (recentShares.length > 0) {
|
||||
setShares((prev) => {
|
||||
const merged = [...recentShares, ...prev];
|
||||
const seen = new Set<string>();
|
||||
return merged.filter((s) => {
|
||||
const key = s.id != null ? String(s.id) : `${s.agent_id}-${s.hash}-${s.timestamp}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
}).slice(0, 20);
|
||||
});
|
||||
}
|
||||
}, [recentShares]);
|
||||
|
||||
const totalHashrate = agents.reduce((sum, a) => sum + a.hashrate_15m, 0);
|
||||
const onlineCount = agents.filter((a) => a.status === 'online').length;
|
||||
const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0);
|
||||
@@ -42,14 +77,35 @@ export default function DashboardPage() {
|
||||
|
||||
const maxAgentHash = Math.max(...topAgents.map((a) => a.hashrate_15m), 1);
|
||||
|
||||
const activityItems = useMemo(
|
||||
() =>
|
||||
shares.slice(0, 12).map((s) => ({
|
||||
id: String(s.id ?? `${s.agent_id}-${s.hash}`),
|
||||
label: s.accepted ? 'OK' : 'BAD',
|
||||
ok: s.accepted,
|
||||
time: s.timestamp ? new Date(s.timestamp).toLocaleTimeString() : undefined,
|
||||
})),
|
||||
[shares]
|
||||
);
|
||||
|
||||
const totalShareCount = agents.reduce((sum, a) => sum + a.shares_total, 0);
|
||||
const alerts = fleetAlerts.length > 0 ? fleetAlerts : restAlerts;
|
||||
const pools = poolStatus.length > 0 ? poolStatus : restPools;
|
||||
const aiEntries = aiActivity.length > 0 ? aiActivity : restAI;
|
||||
const agentNameMap = useMemo(
|
||||
() => Object.fromEntries(agents.map((a) => [a.id, a.name])),
|
||||
[agents]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page fade-in command-deck">
|
||||
<AlertBanner alerts={alerts} />
|
||||
<header className="deck-hero">
|
||||
<div className="deck-hero-text">
|
||||
<p className="deck-eyebrow font-tech">PERSONAL NETWORK · LIVE TELEMETRY</p>
|
||||
<h1>Command Deck</h1>
|
||||
<p className="page-subtitle">
|
||||
Your private mining fleet across the LAN — brass gauges, neon pulse, real-time hashrate.
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="deck-hero-status">
|
||||
@@ -64,6 +120,23 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<NeonCard accent="green" className="section" hud>
|
||||
<h2 className="section-title font-display" style={{ marginBottom: '0.25rem' }}>
|
||||
<span className="section-ornament">◆</span> Fleet Pipeline
|
||||
<span className="section-line" />
|
||||
</h2>
|
||||
<p className="form-hint" style={{ marginTop: 0 }}>
|
||||
Visual progress — lit nodes mean that stage is active. <Link to="/guide">Open Field Guide →</Link>
|
||||
</p>
|
||||
<FleetPipelineStatus
|
||||
hasBuilds={hasBuilds}
|
||||
agentCount={agents.length}
|
||||
onlineCount={onlineCount}
|
||||
hasHashrate={totalHashrate > 0}
|
||||
hasShares={totalShareCount > 0}
|
||||
/>
|
||||
</NeonCard>
|
||||
|
||||
<section className="gauge-row">
|
||||
<NeonCard accent="cyan" className="gauge-card" hud>
|
||||
<GaugeRing
|
||||
@@ -92,6 +165,7 @@ export default function DashboardPage() {
|
||||
<div className="stat-value hashrate neon-glow-cyan">{formatHashrate(totalHashrate)}</div>
|
||||
<div className="stat-sub">{onlineCount} engines firing</div>
|
||||
</NeonCard>
|
||||
<EarningsEstimator hashrate={totalHashrate} />
|
||||
<NeonCard accent="green" className="stat-card-wrap">
|
||||
<div className="stat-label font-tech">Fleet Online</div>
|
||||
<div className="stat-value accepted">{onlineCount} <span className="stat-dim">/ {agents.length}</span></div>
|
||||
@@ -109,6 +183,10 @@ export default function DashboardPage() {
|
||||
</NeonCard>
|
||||
</div>
|
||||
|
||||
<PoolStatusPanel pools={pools} />
|
||||
|
||||
<AIActivityPanel entries={aiEntries} agentNames={agentNameMap} />
|
||||
|
||||
<div className="grid-2 chart-row">
|
||||
<NeonCard accent="cyan" tilt3d>
|
||||
<HashrateChart data={hashHistory} title="Fleet Hashrate Wave" color="#00f5ff" unit="H/s" height={300} />
|
||||
@@ -122,6 +200,14 @@ export default function DashboardPage() {
|
||||
<HashrateChart data={memHistory} title="Memory Load — Fleet Average" color="#ffb020" unit="%" height={220} />
|
||||
</NeonCard>
|
||||
|
||||
<NeonCard accent="purple" className="section" hud>
|
||||
<h2 className="section-title font-display">
|
||||
<span className="section-ornament">◆</span> Share Activity Pulse
|
||||
<span className="section-line" />
|
||||
</h2>
|
||||
<ActivityPulse items={activityItems} />
|
||||
</NeonCard>
|
||||
|
||||
<section className="section">
|
||||
<h2 className="section-title font-display">
|
||||
<span className="section-ornament">◆</span> Machine Roster
|
||||
@@ -164,6 +250,7 @@ export default function DashboardPage() {
|
||||
<div><span>Node</span><strong className="mono-sm">{agent.ip || '—'} · {agent.id.slice(0, 8)}</strong></div>
|
||||
<div><span>Uptime</span><strong>{formatUptime(agent.uptime_seconds)}</strong></div>
|
||||
</div>
|
||||
<AgentRemoteActions agent={agent} compact />
|
||||
</NeonCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user