Final sweep: Crucible fixes, Path Tracer polish, forge progress, tests green.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Align dashboard subtitle default and UpsertAgent tests with fleet label behavior; WebSocket coalesce and PathForge hardening; Crucible expanded ops and visual DV fixes; Vitest 610/610 and full test-suite pass; trim PROBLEMS.md to open items only.
This commit is contained in:
AetherForge
2026-06-06 18:07:47 -07:00
parent e65753ce49
commit 6372b07e6c
40 changed files with 1495 additions and 794 deletions

View File

@@ -320,9 +320,24 @@ export default function AgentRemoteActions({
<div className="compact-offline-banner"> offline commands disabled</div>
)}
<div className="agent-remote-row">
<button type="button" aria-label="Screenshot" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('screenshot')} title="Capture desktop">📷</button>
<button type="button" aria-label="Pause" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('pause')} title="Pause miner"></button>
<button type="button" aria-label="Resume" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('resume')} title="Resume miner"></button>
<button type="button" aria-label="Screenshot" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('screenshot')} title="Capture desktop">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden style={{ width: '1em', height: '1em' }}>
<rect x="3" y="6" width="18" height="14" rx="2" />
<circle cx="12" cy="13" r="3" />
<path d="M9 6l1.5-2h3L15 6" />
</svg>
</button>
<button type="button" aria-label="Pause" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('pause')} title="Pause miner">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden style={{ width: '1em', height: '1em' }}>
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<button type="button" aria-label="Resume" className="agent-action-btn" disabled={!isOnline || !!busy} onClick={() => dispatch('resume')} title="Resume miner">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden style={{ width: '1em', height: '1em' }}>
<path d="M6 4l14 8-14 8V4z" />
</svg>
</button>
<button type="button" aria-label="Reboot" className="agent-action-btn warn" disabled={!isOnline || !!busy} onClick={() => dispatch('reboot_machine')} title="Reboot machine"></button>
<button type="button" aria-label="Shutdown" className="agent-action-btn warn" disabled={!isOnline || !!busy} onClick={() => dispatch('shutdown_machine')} title="Shutdown machine"></button>
<button type="button" aria-label="Wake" className="agent-action-btn" disabled={!!busy} onClick={() => dispatch('wol', { mac: agent?.mac_address })} title="Wake on LAN"></button>
@@ -748,7 +763,12 @@ export default function AgentRemoteActions({
onDragLeave={handleDragLeave}
onDrop={isOnline ? handleDrop : undefined}
>
<span className="drop-icon">📥</span>
<span className="drop-icon" aria-hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: '1.5em', height: '1.5em' }}>
<path d="M12 3v12m0 0l-4-4m4 4l4-4" />
<path d="M4 17v2a2 2 0 002 2h12a2 2 0 002-2v-2" />
</svg>
</span>
<p>Drag &amp; Drop file here</p>
<small>Pushes to user Desktop (any OS)</small>
<button

View File

@@ -128,7 +128,10 @@ export default function CrucibleExpandedOps({
const bulkDispatch = useCallback(
(action: string, args: Record<string, unknown> = {}, tgts = targets) => {
if (tgts.length === 0) return;
if (tgts.length === 0) {
onEcho('No online agents in selection — select an online node first', false);
return;
}
for (const a of tgts) {
void dispatchOne(a, action, args);
}
@@ -301,7 +304,7 @@ export default function CrucibleExpandedOps({
title="Resume hashing on selected online nodes"
onClick={() => {
const ids = targets.map((a) => a.id);
if (ids.length === 0) return;
if (ids.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
api.sendBulkCommand(ids, 'resume').then((r) => onEcho(`resume → sent:${r.sent} failed:${r.failed}`, true)).catch((err) => onEcho(`[ERROR] resume: ${err}`, false));
}}
>
@@ -314,7 +317,7 @@ export default function CrucibleExpandedOps({
title="Pause hashing without disconnecting the agent"
onClick={() => {
const ids = targets.map((a) => a.id);
if (ids.length === 0) return;
if (ids.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
api.sendBulkCommand(ids, 'pause').then((r) => onEcho(`pause → sent:${r.sent} failed:${r.failed}`, true)).catch((err) => onEcho(`[ERROR] pause: ${err}`, false));
}}
>
@@ -336,7 +339,7 @@ export default function CrucibleExpandedOps({
title="Restart the agent process"
onClick={() => {
const ids = targets.map((a) => a.id);
if (ids.length === 0) return;
if (ids.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
api.sendBulkCommand(ids, 'restart').then((r) => onEcho(`restart → sent:${r.sent} failed:${r.failed}`, true)).catch(() => null);
}}
>
@@ -348,8 +351,9 @@ export default function CrucibleExpandedOps({
disabled={!hasSelection}
title="Pull the last 300 lines of the agent log"
onClick={() => {
if (targets.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
targets.forEach((a) => api.sendAgentCommand(a.id, 'get_log', { tail_lines: 300 }).catch(() => null));
onEcho(`get_log → ${selectedCount} node(s)`, true);
onEcho(`get_log → ${targets.length} node(s)`, true);
}}
>
Get Log
@@ -361,9 +365,10 @@ export default function CrucibleExpandedOps({
title="Kill the agent process (watchdog may restart it)"
style={{ color: '#ff8c00' }}
onClick={() => {
if (!window.confirm(`Kill agent process on ${selectedCount} node(s)?`)) return;
if (targets.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
if (!window.confirm(`Kill agent process on ${targets.length} online node(s)?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'stop').catch(() => null));
onEcho(`kill → ${selectedCount} node(s)`, true);
onEcho(`kill → ${targets.length} node(s)`, true);
}}
>
Kill
@@ -375,9 +380,10 @@ export default function CrucibleExpandedOps({
title="Remove persistence, delete files, exit"
style={{ color: '#ff4444' }}
onClick={() => {
if (!window.confirm(`UNINSTALL from ${selectedCount} node(s)? This removes persistence and deletes all agent files.`)) return;
if (targets.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
if (!window.confirm(`UNINSTALL from ${targets.length} online node(s)? This removes persistence and deletes all agent files.`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'uninstall').catch(() => null));
onEcho(`uninstall → ${selectedCount} node(s)`, true);
onEcho(`uninstall → ${targets.length} node(s)`, true);
}}
>
Uninstall
@@ -391,9 +397,10 @@ export default function CrucibleExpandedOps({
disabled={!hasSelection}
title="OS reboot"
onClick={() => {
if (!window.confirm(`Reboot ${selectedCount} machine(s)?`)) return;
if (targets.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
if (!window.confirm(`Reboot ${targets.length} online machine(s)?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'reboot_machine').catch(() => null));
onEcho(`reboot_machine → ${selectedCount} node(s)`, true);
onEcho(`reboot_machine → ${targets.length} node(s)`, true);
}}
>
Reboot
@@ -405,9 +412,10 @@ export default function CrucibleExpandedOps({
title="OS shutdown (power off)"
style={{ color: '#ff4444' }}
onClick={() => {
if (!window.confirm(`Shutdown ${selectedCount} machine(s)?`)) return;
if (targets.length === 0) { onEcho('No online agents selected — pick an online node first', false); return; }
if (!window.confirm(`Shutdown ${targets.length} online machine(s)?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'shutdown_machine').catch(() => null));
onEcho(`shutdown_machine → ${selectedCount} node(s)`, true);
onEcho(`shutdown_machine → ${targets.length} node(s)`, true);
}}
>
Shutdown

View File

@@ -14,6 +14,7 @@ import { getSetupStatus } from '../../help/setupStatus';
import { resolvePageWeather } from '../../help/pageWeather';
import { api } from '../../api/client';
import { usePresence } from '../../context/PresenceContext';
import { useVisualEffects } from '../../context/VisualEffectsContext';
import ComradeAvatar from '../Presence/ComradeAvatar';
import type { ServerConfig, ServerInfo } from '../../types';
import '../Presence/Presence.css';
@@ -42,20 +43,20 @@ const NAV = [
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
{ to: '/agents', label: 'Fleet Roster', icon: 'fleet' },
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
{ to: '/pathtracer', label: 'Path Tracer', icon: 'trace' },
{ to: '/forge', label: 'Forge', icon: 'forge' },
{ to: '/mission-deck', label: 'Mission Deck', icon: 'mission', glow: true },
{ to: '/builds', label: 'Builds', icon: 'builds' },
{ to: '/emberwake', label: 'Emberwake', icon: 'ember' },
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
{ to: '/pathtracer', label: 'Path Tracer', icon: 'trace' },
] as const;
const DOCS_HREF = '/docs/';
/** Primary tabs on iPhone bottom bar */
const MOBILE_PRIMARY = NAV.slice(0, 4);
/** Builds, Calibrate, Path Tracer — “More” sheet */
const MOBILE_MORE = NAV.slice(4);
/** Primary tabs on mobile bottom bar — Deck, Fleet, Crucible, Path Tracer, Forge */
const MOBILE_PRIMARY = NAV.slice(0, 5);
/** Mission Deck, Builds, Emberwake, Calibrate — “More” sheet */
const MOBILE_MORE = NAV.slice(5);
function NavIcon({ type }: { type: string }) {
switch (type) {
@@ -224,6 +225,7 @@ export default function Layout({ children }: LayoutProps) {
const location = useLocation();
const isMobile = useIsMobileLayout();
const { othersOnline, comrades } = usePresence();
const { glowParticles } = useVisualEffects();
const [serverConfig, setServerConfig] = useState<ServerConfig | null>(null);
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
const [moreOpen, setMoreOpen] = useState(false);
@@ -257,6 +259,7 @@ export default function Layout({ children }: LayoutProps) {
'/dashboard': 'Deck',
'/agents': 'Fleet',
'/crucible': 'Ops',
'/pathtracer': 'Tracer',
'/forge': 'Forge',
};
@@ -265,9 +268,9 @@ export default function Layout({ children }: LayoutProps) {
className={`layout${isMobile ? ' layout--mobile' : ''}${othersOnline ? ' layout--comrades-online' : ''}`}
data-operator-deck={operatorDeckId(location.pathname)}
>
{!isMobile && <CursorFire />}
{!isMobile && glowParticles && <CursorFire />}
<AmbientBackground weather={pageWeather} />
<SacredGeometryLayer />
{glowParticles && <SacredGeometryLayer />}
<nav className="sidebar sidebar--desktop desktop-only">
<div className="sidebar-header">
<div className="logo">

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useVisibleInterval } from '../../hooks/usePageVisible';
import { api } from '../../api/client';
import { useWebSocket } from '../../hooks/useWebSocket';
import ComradeIndicators from '../Presence/ComradeIndicators';
import { usePresence } from '../../context/PresenceContext';
import '../Presence/Presence.css';
@@ -8,10 +9,14 @@ import './VisualComponents.css';
export default function SystemStatusBar() {
const [serverOk, setServerOk] = useState(true);
const [agentTotal, setAgentTotal] = useState(0);
const [agentOnline, setAgentOnline] = useState(0);
const [buildCount, setBuildCount] = useState(0);
const { othersOnline } = usePresence();
const { agents } = useWebSocket();
const { agentTotal, agentOnline } = useMemo(() => ({
agentTotal: agents.length,
agentOnline: agents.filter((a) => a.status === 'online').length,
}), [agents]);
const poll = useCallback(async () => {
try {
@@ -20,14 +25,6 @@ export default function SystemStatusBar() {
} catch {
setServerOk(false);
}
try {
const agents = await api.listAgents();
setAgentTotal(agents.length);
setAgentOnline(agents.filter((a) => a.status === 'online').length);
} catch {
setAgentTotal(0);
setAgentOnline(0);
}
try {
const builds = await api.listBuilds();
setBuildCount(builds.length);
@@ -64,9 +61,14 @@ export default function SystemStatusBar() {
target="_blank"
rel="noopener noreferrer"
className="status-pill"
style={{ textDecoration: 'none', color: 'var(--neon-cyan)' }}
style={{ textDecoration: 'none', color: 'var(--neon-cyan)', display: 'inline-flex', alignItems: 'center', gap: '0.3em' }}
>
📖 DOCS
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ width: '0.95em', height: '0.95em', flexShrink: 0 }} aria-hidden>
<path d="M4 19.5A2.5 2.5 0 016.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" />
<path d="M8 7h8M8 11h6" strokeOpacity="0.55" />
</svg>
DOCS
</a>
</div>
);

View File

@@ -770,8 +770,18 @@ describe('SystemStatusBar', () => {
beforeEach(() => {
vi.spyOn(api, 'healthCheck').mockResolvedValue(undefined);
vi.spyOn(api, 'listAgents').mockResolvedValue([mockAgent(), mockAgent({ id: 'a2', status: 'offline' })]);
vi.spyOn(api, 'listBuilds').mockResolvedValue([{ id: 'b1' } as never]);
useWebSocketMock.mockReturnValue({
isConnected: true,
agents: [mockAgent(), mockAgent({ id: 'a2', status: 'offline' })],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
});
});
it('shows server and fleet pills after poll', async () => {
@@ -782,9 +792,9 @@ describe('SystemStatusBar', () => {
);
await waitFor(() => {
expect(screen.getByText(/SERVER UP/i)).toBeInTheDocument();
expect(screen.getByText(/FLEET 1\/2 ONLINE/i)).toBeInTheDocument();
expect(screen.getByText(/1 BUILD/i)).toBeInTheDocument();
});
expect(screen.getByText(/FLEET 1\/2 ONLINE/i)).toBeInTheDocument();
expect(screen.getByText(/1 BUILD/i)).toBeInTheDocument();
});
});