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,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>
);
}