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:
drjones
2026-05-27 09:16:04 -07:00
parent 9d223b8137
commit df81eb7744
75 changed files with 8891 additions and 966 deletions

View File

@@ -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>