Add forge pipeline polish, simple forge UX, and fleet management upgrades.

Fusion copies prep icon and version info via go-winres; optional Garble obfuscation, Authenticode signing, and dry-run size estimates. Forge Simple mode with smart defaults; fleet roster gets compact expandable cards, filters, bulk commands, per-agent notes/tags, and typed WebSocket payloads.
This commit is contained in:
drjones
2026-05-28 21:48:20 -07:00
parent fda72041f0
commit c95a4373de
45 changed files with 2282 additions and 272 deletions

View File

@@ -9,9 +9,17 @@ 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 FleetToolbar from '../components/Fleet/FleetToolbar';
import {
DEFAULT_FLEET_FILTERS,
filterFleetAgents,
agentIsIdleMiner,
formatHashrate,
formatUptime,
} from '../help/fleetFilters';
import type { FleetFilterState } from '../help/fleetFilters';
import '../components/Fleet/AgentRemoteActions.css';
import './Pages.css';
export default function DashboardPage() {
const { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity } = useWebSocket();
const [shares, setShares] = useState<Share[]>([]);
@@ -23,7 +31,9 @@ export default function DashboardPage() {
const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]);
const [memHistory, setMemHistory] = useState<{ time: string; value: number }[]>([]);
const [hasBuilds, setHasBuilds] = useState(false);
const [filters, setFilters] = useState<FleetFilterState>(DEFAULT_FLEET_FILTERS);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [bulkBusy, setBulkBusy] = useState(false);
useEffect(() => {
api.getRecentShares(20).then(setShares).catch(console.error);
api.listBuilds().then((b) => setHasBuilds(b.length > 0)).catch(console.error);
@@ -70,11 +80,12 @@ export default function DashboardPage() {
setMemHistory((prev) => [...prev.slice(-59), { time: now, value: avgMem }]);
}, [totalHashrate, avgCpu, avgMem]);
const topAgents = useMemo(
() => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 8),
[agents]
);
const filteredAgents = useMemo(() => filterFleetAgents(agents, filters), [agents, filters]);
const topAgents = useMemo(
() => [...filteredAgents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 12),
[filteredAgents]
);
const maxAgentHash = Math.max(...topAgents.map((a) => a.hashrate_15m), 1);
const activityItems = useMemo(
@@ -97,8 +108,28 @@ export default function DashboardPage() {
[agents]
);
return (
<div className="page fade-in command-deck">
const handleBulkAction = async (action: string) => {
let targetIds = [...selectedIds];
if (action === 'restart_idle') {
targetIds = agents.filter((a) => selectedIds.has(a.id) && agentIsIdleMiner(a)).map((a) => a.id);
if (targetIds.length === 0) {
alert('No selected online agents with idle hashrate.');
return;
}
action = 'restart';
}
const onlineIds = targetIds.filter((id) => agents.find((a) => a.id === id)?.status === 'online');
if (onlineIds.length === 0) return;
if (action === 'stop' && !window.confirm(`Stop ${onlineIds.length} agent(s)?`)) return;
setBulkBusy(true);
try {
await api.sendBulkCommand(onlineIds, action);
} finally {
setBulkBusy(false);
}
};
return ( <div className="page fade-in command-deck">
<AlertBanner alerts={alerts} />
<header className="deck-hero">
<div className="deck-hero-text">
@@ -213,8 +244,17 @@ export default function DashboardPage() {
<span className="section-ornament"></span> Machine Roster
<span className="section-line" />
</h2>
<div className="agent-grid">
{agents.length === 0 && (
{agents.length > 0 && (
<FleetToolbar
agents={agents}
filters={filters}
onChange={setFilters}
selectedCount={selectedIds.size}
onBulkAction={handleBulkAction}
bulkBusy={bulkBusy}
/>
)}
<div className="agent-grid"> {agents.length === 0 && (
<NeonCard accent="brass" className="empty-state">
<div className="empty-icon"></div>
<h3>No miners on the wire</h3>
@@ -230,12 +270,31 @@ export default function DashboardPage() {
>
<div className="agent-card-header">
<div className="agent-name">
<input
type="checkbox"
className="checkbox"
checked={selectedIds.has(agent.id)}
onChange={(e) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (e.target.checked) next.add(agent.id);
else next.delete(agent.id);
return next;
});
}}
/>
<span className={`status-dot ${agent.status}`} />
<span>{agent.name}</span>
</div>
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
</div>
<div className="agent-hash-bar">
{(agent.tags?.length ?? 0) > 0 && (
<div style={{ marginBottom: '0.35rem' }}>
{agent.tags!.map((t) => (
<span key={t} className="agent-tag-chip">{t}</span>
))}
</div>
)} <div className="agent-hash-bar">
<div
className="agent-hash-fill"
style={{ width: `${(agent.hashrate_15m / maxAgentHash) * 100}%` }}
@@ -250,12 +309,16 @@ 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 />
<AgentRemoteActions agent={agent} compact online={agent.status === 'online'} />
</NeonCard>
))}
{agents.length > 0 && topAgents.length === 0 && (
<NeonCard accent="brass" className="empty-state">
<p>No agents match current filters.</p>
</NeonCard>
)}
</div>
</section>
<section className="section">
<h2 className="section-title font-display">
<span className="section-ornament"></span> Share Log
@@ -298,21 +361,6 @@ export default function DashboardPage() {
);
}
function formatHashrate(h: number): string {
if (h >= 1_000_000) return `${(h / 1_000_000).toFixed(2)} MH/s`;
if (h >= 1_000) return `${(h / 1_000).toFixed(2)} KH/s`;
return `${h.toFixed(0)} H/s`;
}
function formatUptime(seconds: number): string {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function formatTime(t: string): string {
return new Date(t).toLocaleTimeString();
}