feat: Tenable-style patch_status - pending_updates, last_patch, reboot_pending across full stack

This commit is contained in:
AetherForge
2026-05-30 23:11:32 -07:00
parent 9232f4c448
commit 4207f6c21b
43 changed files with 4359 additions and 180 deletions

View File

@@ -115,6 +115,17 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
),
status: 'online' as const,
...(update.ssh_available !== undefined ? { ssh_available: update.ssh_available } : {}),
...(update.posture_score !== undefined ? { posture_score: update.posture_score } : {}),
...(update.last_patch_days !== undefined ? { last_patch_days: update.last_patch_days } : {}),
...(update.defender_rtp !== undefined ? { defender_rtp: update.defender_rtp } : {}),
...(update.av_products !== undefined ? { av_products: update.av_products } : {}),
...(update.firewall_domain !== undefined ? { firewall_domain: update.firewall_domain } : {}),
...(update.firewall_private !== undefined ? { firewall_private: update.firewall_private } : {}),
...(update.firewall_public !== undefined ? { firewall_public: update.firewall_public } : {}),
...(update.last_patch !== undefined ? { last_patch: update.last_patch } : {}),
...(update.pending_updates !== undefined ? { pending_updates: update.pending_updates } : {}),
...(update.reboot_pending !== undefined ? { reboot_pending: update.reboot_pending } : {}),
...(update.agent_elevated !== undefined ? { agent_elevated: update.agent_elevated } : {}),
}
: a
)

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { formatLanEndpoint, lanEndpointCandidates } from './endpointHelpers';
import type { ServerInfo } from '../types';
describe('formatLanEndpoint', () => {
it('strips scheme and path from host', () => {
expect(formatLanEndpoint('https://192.168.1.5:8989/extra', 8989)).toBe('http://192.168.1.5:8989');
});
it('builds http URL from bare IP', () => {
expect(formatLanEndpoint('10.0.0.2', 7777)).toBe('http://10.0.0.2:7777');
});
});
describe('lanEndpointCandidates', () => {
it('dedupes suggested URL and local IPs', () => {
const info: ServerInfo = {
port: 8989,
host: 'localhost',
local_ips: ['192.168.1.10', '192.168.1.10'],
suggested_url: 'http://192.168.1.10:8989',
dashboard_url: 'http://192.168.1.10:8989',
websocket_url: 'ws://192.168.1.10:8989/ws/agent',
};
const urls = lanEndpointCandidates(info);
expect(urls).toEqual(['http://192.168.1.10:8989']);
});
it('honours port override', () => {
const info: ServerInfo = {
port: 8989,
host: 'localhost',
local_ips: ['10.0.0.1'],
suggested_url: '',
dashboard_url: '',
websocket_url: '',
};
expect(lanEndpointCandidates(info, 9000)).toEqual(['http://10.0.0.1:9000']);
});
});

View File

@@ -0,0 +1,133 @@
import { describe, expect, it } from 'vitest';
import {
computeFleetHealth,
contributionBars,
findUnderperformers,
fleetMedianHashrate,
groupBySubnet,
osArchBreakdown,
staleAgentIds,
timeToPayout,
} from './fleetAnalytics';
import type { Agent, PoolStatus } from '../types';
const agent = (overrides: Partial<Agent> = {}): Agent => ({
id: 'a1',
name: 'node-1',
status: 'online',
hashrate_15m: 1000,
shares_total: 100,
shares_good: 98,
ip: '192.168.1.10',
platform: 'windows',
arch: 'amd64',
last_seen: new Date().toISOString(),
...overrides,
} as Agent);
describe('computeFleetHealth', () => {
it('returns NOMINAL for healthy fleet', () => {
const agents = [agent(), agent({ id: 'a2', name: 'node-2', hashrate_15m: 900 })];
const pools: PoolStatus[] = [{ name: 'primary', status: 'green' }];
const health = computeFleetHealth(agents, pools);
expect(health.label).toBe('NOMINAL');
expect(health.score).toBeGreaterThanOrEqual(80);
expect(health.issues).toHaveLength(0);
});
it('flags offline nodes and pool degradation', () => {
const agents = [agent(), agent({ id: 'a2', status: 'offline', hashrate_15m: 0 })];
const pools: PoolStatus[] = [{ name: 'primary', status: 'red' }];
const health = computeFleetHealth(agents, pools);
expect(health.label).not.toBe('NOMINAL');
expect(health.issues.some((i) => i.includes('offline'))).toBe(true);
expect(health.issues.some((i) => i.includes('pool'))).toBe(true);
});
});
describe('contributionBars', () => {
it('sorts online agents by hashrate share', () => {
const bars = contributionBars([
agent({ id: 'a1', hashrate_15m: 300 }),
agent({ id: 'a2', hashrate_15m: 700 }),
agent({ id: 'a3', status: 'offline', hashrate_15m: 9999 }),
]);
expect(bars).toHaveLength(2);
expect(bars[0].id).toBe('a2');
expect(bars[0].pct).toBeCloseTo(70, 1);
});
});
describe('findUnderperformers', () => {
it('returns agents below 70% of median', () => {
const agents = [
agent({ id: 'fast', hashrate_15m: 1000 }),
agent({ id: 'slow', hashrate_15m: 500 }),
agent({ id: 'mid', hashrate_15m: 900 }),
];
const under = findUnderperformers(agents);
expect(under.map((a) => a.id)).toEqual(['slow']);
});
it('returns empty when fewer than 2 online miners', () => {
expect(findUnderperformers([agent()])).toEqual([]);
});
});
describe('fleetMedianHashrate', () => {
it('computes median of online non-zero agents', () => {
const med = fleetMedianHashrate([
agent({ hashrate_15m: 100 }),
agent({ id: 'a2', hashrate_15m: 300 }),
agent({ id: 'a3', hashrate_15m: 200 }),
]);
expect(med).toBe(200);
});
});
describe('groupBySubnet', () => {
it('groups by /24 prefix', () => {
const groups = groupBySubnet([
agent({ ip: '10.0.0.1' }),
agent({ id: 'a2', ip: '10.0.0.2' }),
agent({ id: 'a3', ip: '192.168.5.9' }),
]);
expect(groups).toHaveLength(2);
expect(groups[0].subnet).toBe('10.0.0.x');
expect(groups[0].agents).toHaveLength(2);
});
});
describe('osArchBreakdown', () => {
it('labels platforms for display', () => {
const rows = osArchBreakdown([
agent({ platform: 'linux', arch: 'amd64' }),
agent({ id: 'a2', platform: 'darwin', arch: 'arm64' }),
]);
expect(rows.some((r) => r.label.includes('Linux'))).toBe(true);
expect(rows.some((r) => r.label.includes('macOS'))).toBe(true);
});
});
describe('staleAgentIds', () => {
it('flags online agents not seen in 5+ minutes', () => {
const staleTime = new Date(Date.now() - 6 * 60 * 1000).toISOString();
const ids = staleAgentIds([
agent({ id: 'fresh', last_seen: new Date().toISOString() }),
agent({ id: 'stale', last_seen: staleTime }),
]);
expect(ids.has('stale')).toBe(true);
expect(ids.has('fresh')).toBe(false);
});
});
describe('timeToPayout', () => {
it('returns days until payout', () => {
expect(timeToPayout(0.5, 0.25)).toBe(2);
});
it('returns null when data missing', () => {
expect(timeToPayout(undefined, 1)).toBeNull();
expect(timeToPayout(1, 0)).toBeNull();
});
});

View File

@@ -86,9 +86,9 @@ export function computeFleetHealth(agents: Agent[], pools: PoolStatus[]): FleetH
// ─── Contribution Map ─────────────────────────────────────────────────────────
export function contributionBars(agents: Agent[]): ContributionBar[] {
const total = agents.reduce((s, a) => s + a.hashrate_15m, 0);
return agents
.filter((a) => a.status === 'online')
const online = agents.filter((a) => a.status === 'online');
const total = online.reduce((s, a) => s + a.hashrate_15m, 0);
return online
.map((a) => ({
id: a.id,
name: a.name,

View File

@@ -0,0 +1,273 @@
import { describe, expect, it } from 'vitest';
import { runForgeCompatibilityChecks } from './forgeCompatibility';
import { FORGE_BUILD_DEFAULTS } from './forgeDefaults';
import type { BuildRequest } from '../types';
/** Valid mainnet-style Monero address (95 chars, starts with 4). */
const VALID_WALLET = '4' + 'A'.repeat(94);
function baseForm(overrides: Partial<BuildRequest> = {}): BuildRequest {
return {
worker_name: 'pc-lab-1',
server_url: 'http://192.168.1.10:8989',
wallet: VALID_WALLET,
pool_host: 'pool.supportxmr.com',
pool_port: 3333,
pool_tls: true,
pool_pass: 'x',
...FORGE_BUILD_DEFAULTS,
...overrides,
} as BuildRequest;
}
function hasCheck(form: BuildRequest, fusionPrep: boolean, id: string, level?: string) {
const checks = runForgeCompatibilityChecks(form, fusionPrep);
const match = checks.find((c) => c.id === id);
if (!match) return false;
return level === undefined || match.level === level;
}
describe('runForgeCompatibilityChecks', () => {
it('returns no errors for a fully valid form', () => {
const checks = runForgeCompatibilityChecks(baseForm(), false);
const errors = checks.filter((c) => c.level === 'error');
expect(errors).toHaveLength(0);
expect(checks.some((c) => c.id === 'forge_ready' && c.level === 'ok')).toBe(true);
});
describe('stealth and display', () => {
it('errors when stealth mode uses visible display', () => {
expect(
hasCheck(baseForm({ stealth_mode: true, display_mode: 'visible' }), false, 'stealth_display', 'error')
).toBe(true);
});
it('errors when stealth mode enables file logging', () => {
expect(hasCheck(baseForm({ stealth_mode: true, file_logging: true }), false, 'stealth_logs', 'error')).toBe(
true
);
});
it('errors when fusion uses visible display', () => {
expect(
hasCheck(baseForm({ fusion_enabled: true, display_mode: 'visible' }), false, 'fusion_display', 'error')
).toBe(true);
});
});
describe('idle mining mode', () => {
it('errors when idle threshold is out of range', () => {
expect(
hasCheck(baseForm({ mining_mode: 'idle', idle_threshold_pct: 0 }), false, 'idle_threshold', 'error')
).toBe(true);
expect(
hasCheck(baseForm({ mining_mode: 'idle', idle_threshold_pct: 101 }), false, 'idle_threshold', 'error')
).toBe(true);
});
it('errors when idle duration is below 1 minute', () => {
expect(
hasCheck(baseForm({ mining_mode: 'idle', idle_duration_minutes: 0 }), false, 'idle_duration', 'error')
).toBe(true);
});
it('does not check idle fields when mining mode is not idle', () => {
expect(hasCheck(baseForm({ mining_mode: 'always', idle_threshold_pct: 0 }), false, 'idle_threshold')).toBe(
false
);
});
});
describe('scheduled mining mode', () => {
it('errors when schedule times are missing', () => {
expect(
hasCheck(
baseForm({ mining_mode: 'scheduled', schedule_start: '', schedule_end: '' }),
false,
'schedule',
'error'
)
).toBe(true);
});
it('passes when both schedule times are set', () => {
expect(
hasCheck(
baseForm({ mining_mode: 'scheduled', schedule_start: '22:00', schedule_end: '06:00' }),
false,
'schedule'
)
).toBe(false);
});
});
describe('threads and CPU limits', () => {
it('warns when adapt_to_hardware is on with fixed thread mode', () => {
expect(
hasCheck(baseForm({ thread_mode: 'fixed', adapt_to_hardware: true }), false, 'adapt_fixed', 'warn')
).toBe(true);
});
it('errors when thread percent is out of range', () => {
expect(
hasCheck(baseForm({ thread_mode: 'percent', thread_percent: 0 }), false, 'thread_percent_range', 'error')
).toBe(true);
expect(
hasCheck(baseForm({ thread_mode: 'percent', thread_percent: 101 }), false, 'thread_percent_range', 'error')
).toBe(true);
});
it('errors when max CPU usage is out of range', () => {
expect(hasCheck(baseForm({ max_cpu_usage_pct: 0 }), false, 'max_cpu', 'error')).toBe(true);
expect(hasCheck(baseForm({ max_cpu_usage_pct: 101 }), false, 'max_cpu', 'error')).toBe(true);
});
it('errors when max memory is out of range', () => {
expect(hasCheck(baseForm({ max_memory_percent: 9 }), false, 'max_mem', 'error')).toBe(true);
expect(hasCheck(baseForm({ max_memory_percent: 96 }), false, 'max_mem', 'error')).toBe(true);
});
});
describe('pool configuration', () => {
it('errors when pool port is out of range', () => {
expect(hasCheck(baseForm({ pool_port: 0 }), false, 'pool_port', 'error')).toBe(true);
expect(hasCheck(baseForm({ pool_port: 70000 }), false, 'pool_port', 'error')).toBe(true);
});
it('warns on port 443 without TLS', () => {
expect(hasCheck(baseForm({ pool_port: 443, pool_tls: false }), false, 'pool_tls_443', 'warn')).toBe(true);
});
it('warns on TLS with port 3333', () => {
expect(hasCheck(baseForm({ pool_port: 3333, pool_tls: true }), false, 'pool_tls_3333', 'warn')).toBe(true);
});
it('warns when pool password is empty', () => {
expect(hasCheck(baseForm({ pool_pass: ' ' }), false, 'pool_pass', 'warn')).toBe(true);
});
});
describe('run mode and AI', () => {
it('warns when run_as is service', () => {
expect(hasCheck(baseForm({ run_as: 'service' }), false, 'run_as_service', 'warn')).toBe(true);
});
it('warns when AI uses 127.0.0.1 endpoint', () => {
expect(
hasCheck(
baseForm({ ai_enabled: true, ai_ollama_endpoint: 'http://127.0.0.1:11434' }),
false,
'ai_localhost',
'warn'
)
).toBe(true);
});
it('warns when AI is enabled without self-healing', () => {
expect(hasCheck(baseForm({ ai_enabled: true, self_healing: false }), false, 'ai_no_heal', 'warn')).toBe(true);
});
});
describe('identity fields', () => {
it('errors when process name is empty', () => {
expect(hasCheck(baseForm({ process_name: ' ' }), false, 'process_name_empty', 'error')).toBe(true);
});
it('warns when process name has unusual characters', () => {
expect(hasCheck(baseForm({ process_name: 'bad name!' }), false, 'process_name', 'warn')).toBe(true);
});
it('errors when worker name is empty', () => {
expect(hasCheck(baseForm({ worker_name: '' }), false, 'worker_name_empty', 'error')).toBe(true);
});
it('errors when server URL uses localhost', () => {
expect(
hasCheck(baseForm({ server_url: 'http://localhost:8989' }), false, 'server_url_localhost', 'error')
).toBe(true);
});
it('errors when server URL uses 127.0.0.1', () => {
expect(
hasCheck(baseForm({ server_url: 'http://127.0.0.1:8989' }), false, 'server_url_localhost', 'error')
).toBe(true);
});
it('warns when wallet does not match Monero format', () => {
expect(hasCheck(baseForm({ wallet: 'not-a-wallet' }), false, 'wallet_invalid', 'warn')).toBe(true);
});
it('accepts minimum-length wallet (90 chars)', () => {
const wallet = '4' + 'A'.repeat(89);
expect(wallet.length).toBe(90);
expect(hasCheck(baseForm({ wallet }), false, 'wallet_invalid')).toBe(false);
});
it('accepts subaddress starting with 8', () => {
const subaddress = '8' + 'B'.repeat(94);
expect(hasCheck(baseForm({ wallet: subaddress }), false, 'wallet_invalid')).toBe(false);
});
it('emits forge_ready when core config is coherent', () => {
const checks = runForgeCompatibilityChecks(baseForm(), false);
const ready = checks.find((c) => c.id === 'forge_ready');
expect(ready?.level).toBe('ok');
expect(ready?.message).toContain('Core miner config looks coherent');
});
});
describe('fusion and deliverables', () => {
it('emits fusion_ready when prep is attached', () => {
expect(hasCheck(baseForm({ fusion_enabled: true }), true, 'fusion_ready', 'ok')).toBe(true);
});
it('errors when spread kit targets non-universal OS', () => {
expect(
hasCheck(baseForm({ spread_kit: true, target_os: 'windows' }), false, 'spread_kit_os', 'error')
).toBe(true);
});
it('errors when spread kit and fusion are both enabled', () => {
expect(
hasCheck(baseForm({ spread_kit: true, fusion_enabled: true }), false, 'spread_fusion', 'error')
).toBe(true);
});
it('warns when universal has no spread kit or fusion', () => {
expect(
hasCheck(
baseForm({ target_os: 'universal', spread_kit: false, fusion_enabled: false }),
false,
'universal_deliverable',
'warn'
)
).toBe(true);
});
});
describe('platform-specific constraints', () => {
it('errors when process hollowing is set on Linux', () => {
expect(
hasCheck(baseForm({ target_os: 'linux', process_hollowing: true }), false, 'hollow_unix', 'error')
).toBe(true);
});
it('errors when process hollowing is set on macOS', () => {
expect(
hasCheck(baseForm({ target_os: 'darwin', process_hollowing: true }), false, 'hollow_unix', 'error')
).toBe(true);
});
it('errors when sign_build is set on Linux', () => {
expect(hasCheck(baseForm({ target_os: 'linux', sign_build: true }), false, 'sign_unix', 'error')).toBe(
true
);
});
it('errors when sign_build is set on macOS', () => {
expect(hasCheck(baseForm({ target_os: 'darwin', sign_build: true }), false, 'sign_unix', 'error')).toBe(
true
);
});
});
});

View File

@@ -190,7 +190,7 @@ export function runForgeCompatibilityChecks(form: BuildRequest, fusionPrepSelect
checks.push({
id: 'wallet_invalid',
level: 'warn',
message: 'Wallet address does not match standard Monero format (starting with 4 or 8, length 95-106). Double check it.',
message: 'Wallet address does not match standard Monero format (starting with 4 or 8, length 90-106). Double check it.',
});
}

View File

@@ -0,0 +1,427 @@
import { describe, expect, it } from 'vitest';
import {
FORGE_SECTIONS,
applyForgeFieldUpdate,
forgeBadgeLabel,
getForgeFieldMeta,
getForgeLiveNotices,
type ForgeFieldBadge,
} from './forgeRules';
import { FORGE_BUILD_DEFAULTS } from './forgeDefaults';
import type { BuildRequest } from '../types';
function baseForm(overrides: Partial<BuildRequest> = {}): BuildRequest {
return {
worker_name: 'pc-lab-1',
server_url: 'http://192.168.1.10:8989',
wallet: '4' + 'A'.repeat(94),
pool_host: 'pool.supportxmr.com',
pool_port: 3333,
pool_tls: false,
pool_pass: 'x',
...FORGE_BUILD_DEFAULTS,
...overrides,
} as BuildRequest;
}
describe('FORGE_SECTIONS', () => {
it('defines six sections in order', () => {
expect(FORGE_SECTIONS).toHaveLength(6);
expect(FORGE_SECTIONS.map((s) => s.id)).toEqual([
'identity',
'pool',
'performance',
'install',
'fusion',
'ai',
]);
});
it('each section has id, title, description, and baked badge', () => {
for (const section of FORGE_SECTIONS) {
expect(section.id.length).toBeGreaterThan(0);
expect(section.title.length).toBeGreaterThan(0);
expect(section.description.length).toBeGreaterThan(0);
expect(section.badge).toBe('baked');
}
});
it('uses expected section titles', () => {
const titles = Object.fromEntries(FORGE_SECTIONS.map((s) => [s.id, s.title]));
expect(titles.identity).toBe('Identity');
expect(titles.pool).toBe('Pool Configuration');
expect(titles.performance).toBe('Performance & Resources');
expect(titles.install).toBe('Install & Process');
expect(titles.fusion).toBe('Fusion');
expect(titles.ai).toBe('AI Autonomy');
});
});
describe('forgeBadgeLabel', () => {
const cases: [ForgeFieldBadge, string][] = [
['baked', 'Baked into installer'],
['server-only', 'Server folder only — not in .exe'],
['requires', 'Required when parent option is on'],
];
it.each(cases)('maps %s badge to label', (badge, label) => {
expect(forgeBadgeLabel(badge)).toBe(label);
});
});
describe('applyForgeFieldUpdate', () => {
it('stealth_mode disables logging and forces silent/background', () => {
const out = applyForgeFieldUpdate(
baseForm({ display_mode: 'visible', file_logging: true, silent_mode: false }),
'stealth_mode',
true
);
expect(out.stealth_mode).toBe(true);
expect(out.file_logging).toBe(false);
expect(out.display_mode).toBe('background');
expect(out.silent_mode).toBe(true);
});
it('display_mode visible clears stealth and silent', () => {
const out = applyForgeFieldUpdate(
baseForm({ stealth_mode: true, silent_mode: true }),
'display_mode',
'visible'
);
expect(out.display_mode).toBe('visible');
expect(out.stealth_mode).toBe(false);
expect(out.silent_mode).toBe(false);
});
it('display_mode silent/background enables silent_mode', () => {
expect(applyForgeFieldUpdate(baseForm({ silent_mode: false }), 'display_mode', 'silent').silent_mode).toBe(
true
);
expect(
applyForgeFieldUpdate(baseForm({ silent_mode: false }), 'display_mode', 'background').silent_mode
).toBe(true);
});
it('persistence and auto_start stay linked', () => {
expect(applyForgeFieldUpdate(baseForm({ auto_start: false }), 'persistence', true).auto_start).toBe(true);
expect(applyForgeFieldUpdate(baseForm({ persistence: false }), 'auto_start', true).persistence).toBe(true);
});
it('linux target clears Windows-only flags and fixes install base', () => {
const out = applyForgeFieldUpdate(
baseForm({
target_os: 'windows',
process_hollowing: true,
sign_build: true,
obfuscate: true,
install_base: 'localappdata',
}),
'target_os',
'linux'
);
expect(out.target_os).toBe('linux');
expect(out.target_arch).toBe('amd64');
expect(out.process_hollowing).toBe(false);
expect(out.sign_build).toBe(false);
expect(out.obfuscate).toBe(false);
expect(out.spread_kit).toBe(false);
expect(out.install_base).toBe('xdg_data_home');
});
it('darwin target defaults to arm64', () => {
const out = applyForgeFieldUpdate(baseForm(), 'target_os', 'darwin');
expect(out.target_os).toBe('darwin');
expect(out.target_arch).toBe('arm64');
});
it('windows target restores localappdata from xdg path', () => {
const out = applyForgeFieldUpdate(baseForm({ install_base: 'xdg_data_home' }), 'target_os', 'windows');
expect(out.target_os).toBe('windows');
expect(out.target_arch).toBe('all');
expect(out.install_base).toBe('localappdata');
});
it('universal target on single deliverable normalizes back to windows', () => {
const out = applyForgeFieldUpdate(baseForm(), 'target_os', 'universal');
expect(out.target_os).toBe('windows');
expect(out.target_arch).toBe('all');
});
it('universal target stays when spread kit is active', () => {
const out = applyForgeFieldUpdate(baseForm({ spread_kit: true }), 'target_os', 'universal');
expect(out.target_os).toBe('universal');
expect(out.target_arch).toBe('all');
});
it('fusion_enabled forces background, clears spread kit, and universal target', () => {
const out = applyForgeFieldUpdate(
baseForm({ spread_kit: true, display_mode: 'visible', target_os: 'windows' }),
'fusion_enabled',
true
);
expect(out.fusion_enabled).toBe(true);
expect(out.display_mode).toBe('background');
expect(out.silent_mode).toBe(true);
expect(out.spread_kit).toBe(false);
expect(out.target_os).toBe('universal');
});
it('spread_kit applies full preset and clears fusion', () => {
const out = applyForgeFieldUpdate(baseForm({ fusion_enabled: true }), 'spread_kit', true);
expect(out.spread_kit).toBe(true);
expect(out.fusion_enabled).toBe(false);
expect(out.target_os).toBe('universal');
expect(out.target_arch).toBe('all');
expect(out.run_as).toBe('scheduled');
expect(out.persistence).toBe(true);
expect(out.auto_start).toBe(true);
expect(out.self_healing).toBe(true);
expect(out.stealth_mode).toBe(true);
expect(out.silent_mode).toBe(true);
expect(out.file_logging).toBe(false);
expect(out.firewall_exclusion).toBe(true);
expect(out.display_mode).toBe('background');
expect(out.process_hollowing).toBe(false);
});
it('thread_mode fixed sets minimum threads', () => {
const out = applyForgeFieldUpdate(baseForm({ threads: 0 }), 'thread_mode', 'fixed');
expect(out.thread_mode).toBe('fixed');
expect(out.threads).toBeGreaterThanOrEqual(1);
});
it('thread_mode percent clamps invalid thread_percent', () => {
const out = applyForgeFieldUpdate(baseForm({ thread_percent: 0 }), 'thread_mode', 'percent');
expect(out.thread_percent).toBe(75);
});
it('install_base non-custom clears custom path', () => {
const out = applyForgeFieldUpdate(
baseForm({ install_custom_base: 'C:\\custom', install_base: 'custom' }),
'install_base',
'localappdata'
);
expect(out.install_base).toBe('localappdata');
expect(out.install_custom_base).toBe('');
});
it('run_as scheduled/service forces persistence flags', () => {
const scheduled = applyForgeFieldUpdate(baseForm({ persistence: false, auto_start: false }), 'run_as', 'scheduled');
expect(scheduled.persistence).toBe(true);
expect(scheduled.auto_start).toBe(true);
const service = applyForgeFieldUpdate(baseForm({ persistence: false }), 'run_as', 'service');
expect(service.persistence).toBe(true);
expect(service.auto_start).toBe(true);
});
it('ai_enabled fills default endpoint and model when empty', () => {
const out = applyForgeFieldUpdate(
baseForm({ ai_ollama_endpoint: '', ai_model: '' }),
'ai_enabled',
true
);
expect(out.ai_enabled).toBe(true);
expect(out.ai_ollama_endpoint).toBe('http://localhost:11434');
expect(out.ai_model).toBe('llama3.2');
});
it('pool_port 443 auto-enables TLS', () => {
const out = applyForgeFieldUpdate(baseForm({ pool_tls: false }), 'pool_port', 443);
expect(out.pool_port).toBe(443);
expect(out.pool_tls).toBe(true);
});
it('worker_name update does not auto-derive process_name', () => {
const out = applyForgeFieldUpdate(baseForm({ process_name: 'RuntimeBrokerHelper' }), 'worker_name', 'new-worker');
expect(out.worker_name).toBe('new-worker');
expect(out.process_name).toBe('RuntimeBrokerHelper');
});
});
describe('getForgeFieldMeta', () => {
it('returns meta for all known forge fields', () => {
const meta = getForgeFieldMeta(baseForm());
const expectedKeys = [
'worker_name',
'server_url',
'wallet',
'output_dir',
'pool_host',
'pool_port',
'thread_mode',
'thread_percent',
'threads',
'fusion_enabled',
'spread_kit',
'target_os',
'target_arch',
'obfuscate',
'sign_build',
];
for (const key of expectedKeys) {
expect(meta[key]).toBeDefined();
}
});
it('disables thread_percent when thread mode is fixed', () => {
const meta = getForgeFieldMeta(baseForm({ thread_mode: 'fixed' }));
expect(meta.thread_percent.disabled).toBe(true);
expect(meta.threads.disabled).toBe(false);
expect(meta.adapt_to_hardware.disabled).toBe(true);
});
it('disables threads when thread mode is percent', () => {
const meta = getForgeFieldMeta(baseForm({ thread_mode: 'percent' }));
expect(meta.threads.disabled).toBe(true);
expect(meta.thread_percent.disabled).toBe(false);
});
it('disables idle fields unless mining mode is idle', () => {
const idle = getForgeFieldMeta(baseForm({ mining_mode: 'idle' }));
expect(idle.idle_threshold_pct.disabled).toBe(false);
expect(idle.idle_duration_minutes.disabled).toBe(false);
const always = getForgeFieldMeta(baseForm({ mining_mode: 'always' }));
expect(always.idle_threshold_pct.disabled).toBe(true);
expect(always.idle_duration_minutes.disabled).toBe(true);
});
it('disables schedule fields unless mining mode is scheduled', () => {
const scheduled = getForgeFieldMeta(baseForm({ mining_mode: 'scheduled' }));
expect(scheduled.schedule_start.disabled).toBe(false);
expect(scheduled.schedule_end.disabled).toBe(false);
const always = getForgeFieldMeta(baseForm({ mining_mode: 'always' }));
expect(always.schedule_start.disabled).toBe(true);
});
it('locks file_logging under stealth mode', () => {
const meta = getForgeFieldMeta(baseForm({ stealth_mode: true }));
expect(meta.file_logging.disabled).toBe(true);
expect(meta.file_logging.lockedReason).toContain('Stealth mode');
});
it('locks persistence under scheduled/service run_as', () => {
const meta = getForgeFieldMeta(baseForm({ run_as: 'scheduled' }));
expect(meta.persistence.disabled).toBe(true);
expect(meta.auto_start.disabled).toBe(true);
});
it('locks fusion when spread kit is on and vice versa', () => {
const spread = getForgeFieldMeta(baseForm({ spread_kit: true }));
expect(spread.fusion_enabled.disabled).toBe(true);
expect(spread.target_os.disabled).toBe(true);
const fusion = getForgeFieldMeta(baseForm({ fusion_enabled: true }));
expect(fusion.spread_kit.disabled).toBe(true);
expect(fusion.target_os.disabled).toBe(true);
});
it('requires fusion fields only when fusion is enabled', () => {
const off = getForgeFieldMeta(baseForm({ fusion_enabled: false }));
expect(off.fusion_prep.disabled).toBe(true);
expect(off.fusion_run_order.badge).toBe('requires');
const on = getForgeFieldMeta(baseForm({ fusion_enabled: true }));
expect(on.fusion_prep.disabled).toBe(false);
});
it('disables unix-incompatible fields on linux/darwin', () => {
const linux = getForgeFieldMeta(baseForm({ target_os: 'linux' }));
expect(linux.process_hollowing.disabled).toBe(true);
expect(linux.obfuscate.disabled).toBe(true);
expect(linux.target_arch.disabled).toBe(false);
const windows = getForgeFieldMeta(baseForm({ target_os: 'windows' }));
expect(windows.target_arch.disabled).toBe(true);
expect(windows.sign_build.disabled).toBe(false);
});
it('marks output_dir and obfuscate as server-only', () => {
const meta = getForgeFieldMeta(baseForm());
expect(meta.output_dir.badge).toBe('server-only');
expect(meta.obfuscate.badge).toBe('server-only');
});
});
describe('getForgeLiveNotices', () => {
it('returns empty array for a plain windows form', () => {
const notices = getForgeLiveNotices(baseForm({ target_os: 'windows', spread_kit: false }), false);
expect(notices).toEqual([]);
});
it('warns about service run mode', () => {
const notices = getForgeLiveNotices(baseForm({ run_as: 'service' }), false);
expect(notices.some((n) => n.includes('scheduled task'))).toBe(true);
});
it('warns when scheduled/service lacks persistence', () => {
const notices = getForgeLiveNotices(
baseForm({ run_as: 'scheduled', persistence: false }),
false
);
expect(notices.some((n) => n.includes('Persistence is forced on'))).toBe(true);
});
it('warns when fusion enabled without prep upload', () => {
const notices = getForgeLiveNotices(baseForm({ fusion_enabled: true }), false);
expect(notices.some((n) => n.includes('upload prep.exe'))).toBe(true);
});
it('includes AI Ollama notice when AI is enabled', () => {
const notices = getForgeLiveNotices(baseForm({ ai_enabled: true }), false);
expect(notices.some((n) => n.includes('Ollama'))).toBe(true);
});
it('warns when fixed threads ignores adapt_to_hardware', () => {
const notices = getForgeLiveNotices(
baseForm({ thread_mode: 'fixed', adapt_to_hardware: true }),
false
);
expect(notices.some((n) => n.includes('Adapt to hardware'))).toBe(true);
});
it('warns on pool TLS/port mismatches', () => {
const p443 = getForgeLiveNotices(baseForm({ pool_port: 443, pool_tls: false }), false);
expect(p443.some((n) => n.includes('Port 443'))).toBe(true);
const p3333 = getForgeLiveNotices(baseForm({ pool_port: 3333, pool_tls: true }), false);
expect(p3333.some((n) => n.includes('3333'))).toBe(true);
});
it('warns on low max CPU with high thread percent', () => {
const notices = getForgeLiveNotices(
baseForm({ max_cpu_usage_pct: 20, thread_percent: 80, thread_mode: 'percent' }),
false
);
expect(notices.some((n) => n.includes('throttling'))).toBe(true);
});
it('describes universal, spread kit, and fusion deliverables', () => {
const universal = getForgeLiveNotices(baseForm({ target_os: 'universal' }), false);
expect(universal.some((n) => n.includes('Windows, Linux, and macOS'))).toBe(true);
const spread = getForgeLiveNotices(baseForm({ spread_kit: true, target_os: 'universal' }), false);
expect(spread.some((n) => n.includes('Spread Kit'))).toBe(true);
const fusion = getForgeLiveNotices(baseForm({ fusion_enabled: true, target_os: 'universal' }), true);
expect(fusion.some((n) => n.includes('Fusion builds a universal ZIP'))).toBe(true);
});
it('notes single-platform unix install paths', () => {
const linux = getForgeLiveNotices(baseForm({ target_os: 'linux' }), false);
expect(linux.some((n) => n.includes('linux worker'))).toBe(true);
const darwin = getForgeLiveNotices(baseForm({ target_os: 'darwin' }), false);
expect(darwin.some((n) => n.includes('darwin worker'))).toBe(true);
});
it('warns on universal without deliverable type', () => {
const notices = getForgeLiveNotices(
baseForm({ target_os: 'universal', fusion_enabled: false, spread_kit: false }),
false
);
expect(notices.some((n) => n.includes('without Spread Kit or Fusion'))).toBe(true);
});
});

View File

@@ -0,0 +1,133 @@
import { describe, expect, it } from 'vitest';
import { FIELD_HELP, SETUP_CHEATSHEET } from './settingHelp';
describe('SETUP_CHEATSHEET', () => {
it('defines four setup steps in order', () => {
expect(SETUP_CHEATSHEET).toHaveLength(4);
expect(SETUP_CHEATSHEET.map((s) => s.title)).toEqual([
'1. Calibrate once',
'2. Forge (Simple mode)',
'3. Deploy',
'4. Watch the fleet',
]);
});
it('each step has non-empty title and body', () => {
for (const step of SETUP_CHEATSHEET) {
expect(step.title.trim().length).toBeGreaterThan(0);
expect(step.body.trim().length).toBeGreaterThan(20);
}
});
it('mentions key workflow concepts in bodies', () => {
const bodies = SETUP_CHEATSHEET.map((s) => s.body).join(' ');
expect(bodies).toContain('Calibrate');
expect(bodies).toContain('Forge');
expect(bodies).toContain('Command Deck');
expect(bodies).toContain('Fleet Roster');
});
});
describe('FIELD_HELP', () => {
const expectedKeys = [
'calibrate_wallet',
'calibrate_quick_setup',
'forge_simple_mode',
'forge_recommended_defaults',
'obfuscate',
'sign_build',
'obfuscate_default',
'sign_enabled',
'sign_cert_thumbprint',
'sign_tool_path',
'sign_timestamp_url',
'worker_name',
'server_url',
'output_dir',
'wallet',
'pool_host',
'pool_port',
'pool_tls',
'pool_pass',
'threads',
'thread_mode',
'thread_percent',
'max_cpu_usage_pct',
'max_memory_percent',
'min_free_ram_mb',
'cpu_priority',
'mining_mode',
'idle_threshold_pct',
'idle_duration_minutes',
'schedule_start',
'schedule_end',
'display_mode',
'process_name',
'persistence',
'run_as',
'silent_mode',
'auto_start',
'fusion_enabled',
'fusion_run_order',
'fusion_prep',
'fusion_media_mode',
'fusion_output_name',
'fusion_batch',
'install_base',
'install_custom_base',
'install_relative_path',
'public_url',
'websocket_ping_seconds',
'log_pool_traffic',
'adapt_to_hardware',
'self_healing',
'firewall_exclusion',
'open_firewall_on_start',
'file_logging',
'stealth_mode',
'ai_enabled',
'ai_ollama_endpoint',
'ai_model',
'process_hollowing',
'mesh_p2p',
'auto_spread',
'usb_spread',
'share_spread',
'hole_punch',
'remote_aggressive',
'target_os',
'target_arch',
'spread_kit',
'forge_deliverable',
] as const;
it('defines help text for every documented field key', () => {
expect(Object.keys(FIELD_HELP).sort()).toEqual([...expectedKeys].sort());
});
it('each help entry is a non-empty string', () => {
for (const key of expectedKeys) {
const text = FIELD_HELP[key];
expect(typeof text).toBe('string');
expect(text.trim().length).toBeGreaterThan(10);
}
});
it('wallet and server_url entries warn against localhost', () => {
expect(FIELD_HELP.wallet).toMatch(/Monero|wallet/i);
expect(FIELD_HELP.server_url).toMatch(/Not localhost|not localhost/i);
expect(FIELD_HELP.public_url).toMatch(/not localhost/i);
});
it('fusion entries describe decoy bundling', () => {
expect(FIELD_HELP.fusion_enabled).toContain('Fuse the miner');
expect(FIELD_HELP.fusion_prep).toContain('decoy');
expect(FIELD_HELP.fusion_run_order).toContain('Parallel');
});
it('advanced spread entries describe lateral/USB/share behavior', () => {
expect(FIELD_HELP.auto_spread).toContain('SMB');
expect(FIELD_HELP.usb_spread).toContain('USB');
expect(FIELD_HELP.share_spread).toContain('share');
});
});

View File

@@ -0,0 +1,215 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AgentsPage from './AgentsPage';
import { mockAgent, mockServerInfo } from '../test/fixtures';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
vi.mock('../hooks/useWebSocket', () => ({
useWebSocket: vi.fn(),
}));
const useWebSocketMock = vi.mocked(useWebSocket);
function wsValue(overrides: Partial<ReturnType<typeof useWebSocket>> = {}) {
return {
isConnected: false,
agents: [],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
...overrides,
};
}
function renderAgentsPage() {
return render(<AgentsPage />);
}
describe('AgentsPage', () => {
beforeEach(() => {
vi.clearAllMocks();
useWebSocketMock.mockReturnValue(wsValue());
vi.spyOn(api, 'listAgents').mockResolvedValue([]);
vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo);
vi.spyOn(api, 'getAgentStats').mockResolvedValue([]);
vi.spyOn(api, 'getAgentLog').mockResolvedValue({ agent_id: 'x', content: 'log line' });
vi.spyOn(api, 'updateAgentMeta').mockResolvedValue({
success: true,
agent: mockAgent({ notes: 'saved note', tags: ['rack-a'] }),
});
vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({
success: true,
sent: 1,
failed: 0,
action: 'restart',
});
});
afterEach(() => {
cleanup();
});
it('renders page heading and quick deploy labels', async () => {
renderAgentsPage();
expect(screen.getByRole('heading', { level: 1, name: 'Fleet Roster' })).toBeInTheDocument();
expect(screen.getByText('FLEET REGISTRY')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('One-liner Quick Deploy')).toBeInTheDocument();
});
expect(screen.getByText('Install & run (auto-launches)')).toBeInTheDocument();
expect(screen.getByText('Direct download only (saves file)')).toBeInTheDocument();
expect(screen.getAllByText('Windows')).toHaveLength(2);
expect(screen.getByText('Linux/Mac')).toBeInTheDocument();
expect(screen.getByText('macOS')).toBeInTheDocument();
});
it('builds quick deploy URLs from server info', async () => {
renderAgentsPage();
await waitFor(() => {
expect(screen.getAllByText(`iex (irm '${mockServerInfo.suggested_url}/install.ps1')`)).toHaveLength(1);
});
expect(screen.getByText(`curl -sL ${mockServerInfo.suggested_url}/install.sh | bash`)).toBeInTheDocument();
expect(screen.getByText(`${mockServerInfo.suggested_url}/get?os=windows`)).toBeInTheDocument();
});
it('shows loading then empty multi-OS state', async () => {
renderAgentsPage();
expect(screen.getByText('Scanning network...')).toBeInTheDocument();
expect(await screen.findByText('No agents registered')).toBeInTheDocument();
expect(
screen.getByText(/Deploy a worker to any machine \(Windows, Linux, or macOS\)/)
).toBeInTheDocument();
});
it('surfaces listAgents load errors', async () => {
vi.spyOn(api, 'listAgents').mockRejectedValue(new Error('API unavailable'));
renderAgentsPage();
expect(await screen.findByText('API unavailable')).toBeInTheDocument();
});
it('lists agents and opens detail panel with section headings', async () => {
const agent = mockAgent({ name: 'Rack B Miner', notes: 'basement', tags: ['home'] });
vi.spyOn(api, 'listAgents').mockResolvedValue([agent]);
renderAgentsPage();
await waitFor(() => {
expect(screen.getByText('Rack B Miner')).toBeInTheDocument();
});
await userEvent.setup().click(screen.getByText('Rack B Miner'));
await waitFor(() => {
expect(screen.getByRole('heading', { level: 2, name: 'Rack B Miner' })).toBeInTheDocument();
});
expect(screen.getByText('Notes & Tags')).toBeInTheDocument();
expect(screen.getByText('Hashrate')).toBeInTheDocument();
expect(screen.getByText('Shares')).toBeInTheDocument();
expect(screen.getByText('Remote Control')).toBeInTheDocument();
expect(api.getAgentStats).toHaveBeenCalledWith(agent.id, 60);
});
it('preserves notes draft while typing until agent switch', async () => {
const a1 = mockAgent({ id: 'a1', name: 'Node One', notes: 'note one' });
const a2 = mockAgent({ id: 'a2', name: 'Node Two', notes: 'note two' });
vi.spyOn(api, 'listAgents').mockResolvedValue([a1, a2]);
renderAgentsPage();
await waitFor(() => expect(screen.getByText('Node One')).toBeInTheDocument());
const user = userEvent.setup();
await user.click(screen.getByText('Node One'));
const detail = await screen.findByRole('heading', { level: 2, name: 'Node One' });
const panel = detail.closest('.agent-detail') as HTMLElement;
const notes = within(panel).getByPlaceholderText('Notes about this machine…') as HTMLTextAreaElement;
await waitFor(() => expect(notes.value).toBe('note one'));
await user.clear(notes);
await user.type(notes, 'typing in progress');
expect(notes.value).toBe('typing in progress');
await user.click(screen.getByText('Node Two'));
await waitFor(() => expect(notes.value).toBe('note two'));
});
it('saves notes and tags via API', async () => {
const agent = mockAgent({ id: 'save-me', name: 'Save Target' });
vi.spyOn(api, 'listAgents').mockResolvedValue([agent]);
const updateSpy = vi.spyOn(api, 'updateAgentMeta').mockResolvedValue({
success: true,
agent: { ...agent, notes: 'Living room PC', tags: ['living-room'] },
});
renderAgentsPage();
await waitFor(() => expect(screen.getByText('Save Target')).toBeInTheDocument());
const user = userEvent.setup();
await user.click(screen.getByText('Save Target'));
const detail = await screen.findByRole('heading', { level: 2, name: 'Save Target' });
const panel = detail.closest('.agent-detail') as HTMLElement;
const notes = within(panel).getByPlaceholderText('Notes about this machine…');
await user.clear(notes);
await user.type(notes, 'Living room PC');
const tags = within(panel).getByPlaceholderText('Tags: living-room, rack-b (comma separated)');
await user.clear(tags);
await user.type(tags, 'living-room, rack-b');
await user.click(within(panel).getByRole('button', { name: 'Save notes & tags' }));
await waitFor(() => {
expect(updateSpy).toHaveBeenCalledWith('save-me', 'Living room PC', ['living-room', 'rack-b']);
});
expect(await within(panel).findByText('Saved')).toBeInTheDocument();
});
it('alerts when bulk action has no online agents', async () => {
const offline = mockAgent({ id: 'off-1', name: 'Offline Node', status: 'offline' });
vi.spyOn(api, 'listAgents').mockResolvedValue([offline]);
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
renderAgentsPage();
await waitFor(() => expect(screen.getByText('Offline Node')).toBeInTheDocument());
const list = screen.getByText('Offline Node').closest('.agents-list') as HTMLElement;
await userEvent.setup().click(within(list).getByRole('checkbox'));
await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
await waitFor(() => {
expect(alertSpy).toHaveBeenCalledWith('No online agents in selection.');
});
alertSpy.mockRestore();
});
it('alerts when bulk command API fails', async () => {
const agent = mockAgent({ name: 'Online One' });
vi.spyOn(api, 'listAgents').mockResolvedValue([agent]);
vi.spyOn(api, 'sendBulkCommand').mockRejectedValue(new Error('bulk failed'));
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
renderAgentsPage();
await waitFor(() => expect(screen.getByText('Online One')).toBeInTheDocument());
const list = screen.getByText('Online One').closest('.agents-list') as HTMLElement;
await userEvent.setup().click(within(list).getByRole('checkbox'));
await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
await waitFor(() => {
expect(alertSpy).toHaveBeenCalledWith('bulk failed');
});
alertSpy.mockRestore();
});
it('syncs agents from websocket when connected', async () => {
const restAgent = mockAgent({ id: 'rest', name: 'REST Name', hashrate_15m: 100 });
vi.spyOn(api, 'listAgents').mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve([restAgent]), 50))
);
const liveAgent = mockAgent({ id: 'rest', name: 'Live Name', hashrate_15m: 999 });
useWebSocketMock.mockReturnValue(wsValue({ isConnected: true, agents: [liveAgent] }));
renderAgentsPage();
await waitFor(() => {
expect(screen.getByText('Live Name')).toBeInTheDocument();
});
expect(screen.queryByText('REST Name')).not.toBeInTheDocument();
});
it('shows filter empty hint when no agents match', async () => {
vi.spyOn(api, 'listAgents').mockResolvedValue([mockAgent({ name: 'Hidden', tags: ['prod'] })]);
renderAgentsPage();
await waitFor(() => expect(screen.getByText('Hidden')).toBeInTheDocument());
const search = screen.getByPlaceholderText('Search name, IP, notes, tags…');
await userEvent.setup().type(search, 'nomatchxyz');
expect(screen.getByText('No agents match filters.')).toBeInTheDocument();
});
});

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { api } from '../api/client';
import { useWebSocket } from '../hooks/useWebSocket';
import type { Agent, HashrateSample, ServerInfo } from '../types';
@@ -93,13 +93,25 @@ export default function AgentsPage() {
const [tagsDraft, setTagsDraft] = useState('');
const [metaSaving, setMetaSaving] = useState(false);
const [metaMsg, setMetaMsg] = useState('');
const isConnectedRef = useRef(isConnected);
isConnectedRef.current = isConnected;
useEffect(() => {
let cancelled = false;
api.listAgents()
.then(setAgents)
.catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load agents'))
.finally(() => setLoading(false));
.then((data) => {
if (!cancelled && !isConnectedRef.current) setAgents(data);
})
.catch((err) => {
if (!cancelled) setLoadError(err instanceof Error ? err.message : 'Failed to load agents');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
api.getServerInfo().then(setServerInfo).catch(() => {});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
@@ -214,6 +226,7 @@ export default function AgentsPage() {
await api.sendBulkCommand(onlineIds, action);
} catch (err) {
console.error(err);
alert(err instanceof Error ? err.message : 'Bulk command failed');
} finally {
setBulkBusy(false);
}

View File

@@ -150,10 +150,82 @@
margin-top: 1px;
}
.cn-badges {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 2px;
}
.cn-ssh.ssh-on { color: var(--neon-green); background: rgba(57,255,20,0.12); }
.cn-ssh.ssh-off { color: #ff4466; background: rgba(255,68,102,0.12); }
.cn-ssh.ssh-unk { color: var(--text-muted); background: rgba(255,255,255,0.06); }
.cn-posture {
font-size: 0.68rem;
font-family: var(--font-tech);
letter-spacing: 0.04em;
padding: 1px 5px;
border-radius: 3px;
}
.cn-posture.posture-good { color: var(--neon-green); background: rgba(57,255,20,0.1); }
.cn-posture.posture-warn { color: var(--neon-amber); background: rgba(255,176,32,0.12); }
.cn-posture.posture-bad { color: #ff4466; background: rgba(255,68,102,0.12); }
.cn-posture.posture-unk { color: var(--text-muted); background: rgba(255,255,255,0.06); }
.cn-patch {
font-size: 0.65rem;
font-family: var(--font-tech);
padding: 1px 5px;
border-radius: 3px;
}
.cn-patch.patch-ok { color: var(--neon-cyan); background: rgba(0,245,255,0.08); }
.cn-patch.patch-stale { color: var(--neon-amber); background: rgba(255,176,32,0.1); }
.cn-elevated {
font-size: 0.62rem;
font-family: var(--font-tech);
padding: 1px 4px;
border-radius: 3px;
color: var(--neon-magenta);
background: rgba(255,45,166,0.12);
letter-spacing: 0.04em;
}
/* ── Pending-updates badge ─────────────────────────────────────────────────── */
.cn-upd {
font-size: 0.62rem;
font-family: var(--font-tech);
padding: 1px 4px;
border-radius: 3px;
letter-spacing: 0.04em;
}
.cn-upd.upd-ok { color: #00ff88; background: rgba(0,255,136,0.08); }
.cn-upd.upd-warn { color: var(--neon-amber); background: rgba(255,176,32,0.1); }
.cn-upd.upd-bad { color: #ff4444; background: rgba(255,68,68,0.12); font-weight: 700; }
.cn-upd.upd-unk { color: #888; background: rgba(128,128,128,0.08); }
/* ── Reboot-pending badge ──────────────────────────────────────────────────── */
.cn-reboot {
font-size: 0.62rem;
font-family: var(--font-tech);
padding: 1px 4px;
border-radius: 3px;
letter-spacing: 0.04em;
}
.cn-reboot.rb-pending {
color: #ff2222;
background: rgba(255,34,34,0.15);
font-weight: 700;
animation: rb-blink 1.4s step-end infinite;
}
@keyframes rb-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
/* ── Row: Groups + Actions ───────────────────────────────────────────── */
.crucible-row {

View File

@@ -45,6 +45,57 @@ function sshBadge(agent: Agent) {
return { label: 'SSH ?', cls: 'ssh-unk' };
}
function postureBadge(score?: number) {
if (score === undefined) return { label: 'POSTURE ?', cls: 'posture-unk' };
if (score >= 80) return { label: `P:${score}`, cls: 'posture-good' };
if (score >= 40) return { label: `P:${score}`, cls: 'posture-warn' };
return { label: `P:${score}`, cls: 'posture-bad' };
}
function patchLabel(days?: number) {
if (days === undefined) return null;
return { label: `${days}d`, cls: days <= 30 ? 'patch-ok' : 'patch-stale' };
}
function postureTooltip(agent: Agent): string {
const lines: string[] = [];
const yn = (v?: boolean) => v === true ? '✓' : v === false ? '✗' : '?';
const na = (v: unknown) => v !== undefined && v !== null ? String(v) : '?';
lines.push(`Defender: ${yn(agent.defender_enabled)} RTP: ${yn(agent.defender_rtp)}`);
if (agent.av_products?.length) lines.push(`AV: ${agent.av_products.join(', ')}`);
lines.push(`FW Domain:${yn(agent.firewall_domain)} Private:${yn(agent.firewall_private)} Public:${yn(agent.firewall_public)}`);
lines.push(`SSH: ${yn(agent.ssh_available)} Elevated: ${yn(agent.agent_elevated)}`);
lines.push('──────────────────────');
// Patch exposure
if (agent.last_patch) lines.push(`Last patch: ${agent.last_patch} (${na(agent.last_patch_days)}d ago)`);
else if (agent.last_patch_days !== undefined) lines.push(`Last patch: ${agent.last_patch_days}d ago`);
if (agent.pending_updates !== undefined) {
const u = agent.pending_updates;
lines.push(`Pending updates: ${u < 0 ? 'unknown' : u === 0 ? 'none ✓' : `${u}`}`);
}
if (agent.reboot_pending !== undefined) {
lines.push(`Reboot required: ${agent.reboot_pending ? 'YES ⚠' : 'no ✓'}`);
}
return lines.join('\n');
}
function pendingBadge(agent: Agent): { label: string; cls: string } | null {
const u = agent.pending_updates;
if (u === undefined) return null;
if (u < 0) return { label: 'UPD ?', cls: 'upd-unk' };
if (u === 0) return { label: 'UP TO DATE', cls: 'upd-ok' };
if (u <= 5) return { label: `${u} UPD`, cls: 'upd-warn' };
return { label: `${u} UPD`, cls: 'upd-bad' };
}
function rebootBadge(agent: Agent): { label: string; cls: string } | null {
if (agent.reboot_pending === undefined) return null;
if (agent.reboot_pending) return { label: 'REBOOT!', cls: 'rb-pending' };
return null; // no badge when not pending — cleaner UI
}
function platformIcon(platform?: string): string {
if (!platform) return '⬡';
const p = platform.toLowerCase();
@@ -99,8 +150,9 @@ export default function CruciblePage() {
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
const [histIdx, setHistIdx] = useState(-1);
// SSH status overrides (from probe results)
// SSH / posture overrides (from on-demand probes)
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
const allIds = useMemo(() => agents.map((a) => a.id), [agents]);
const selectedAgents = useMemo(
@@ -128,20 +180,35 @@ export default function CruciblePage() {
for (const r of newEntries) {
const aid = r.agent_id;
if (!aid) continue;
// Only show results from agents that are selected (or all if nothing selected)
if (selectedIds.size > 0 && !selectedIds.has(aid)) continue;
const agent = agents.find((a) => a.id === aid);
const name = agent?.name ?? aid.slice(0, 8);
// Parse SSH probe results to update ssh status
const msg = r.message ?? '';
// Always update badges from probe / heartbeat command responses
if (msg.includes('SSH_PROBE:ONLINE')) {
setSshOverride((prev) => ({ ...prev, [aid]: true }));
} else if (msg.includes('SSH_PROBE:OFFLINE')) {
setSshOverride((prev) => ({ ...prev, [aid]: false }));
}
if (r.action === 'posture' || msg.includes('{')) {
try {
const start = msg.indexOf('{');
if (start >= 0) {
const p = JSON.parse(msg.slice(start)) as { posture_score?: number; last_patch_days?: number; ssh_listening?: boolean };
if (typeof p.posture_score === 'number') {
setPostureOverride((prev) => ({
...prev,
[aid]: { score: p.posture_score!, patchDays: p.last_patch_days },
}));
}
if (p.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
if (p.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
}
} catch { /* ignore malformed JSON */ }
}
if (selectedIds.size > 0 && !selectedIds.has(aid)) continue;
const agent = agents.find((a) => a.id === aid);
const name = agent?.name ?? aid.slice(0, 8);
// Split multi-line output
const msgLines = msg.split('\n').filter(Boolean);
for (const line of msgLines) {
lines.push({
@@ -258,6 +325,28 @@ export default function CruciblePage() {
}
};
const probePosture = (targets?: Agent[]) => {
const tgts = targets ?? selectedAgents.filter(online);
Promise.all(
tgts.map((a) =>
api.sendAgentCommand(a.id, 'posture').catch((err) => {
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: a.id,
agentName: a.name,
isCmd: false,
text: `[ERROR] posture probe: ${err instanceof Error ? err.message : String(err)}`,
ts: new Date(),
success: false,
},
]);
})
)
);
};
const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { sendCmd(); return; }
if (e.key === 'ArrowUp') {
@@ -282,6 +371,13 @@ export default function CruciblePage() {
return sshBadge({ ...a });
};
const postureStatus = (a: Agent) => {
const o = postureOverride[a.id];
const score = o?.score ?? a.posture_score;
const patchDays = o?.patchDays ?? a.last_patch_days;
return { ...postureBadge(score), patch: patchLabel(patchDays) };
};
// ── Render ─────────────────────────────────────────────────────────────
return (
@@ -319,6 +415,7 @@ export default function CruciblePage() {
const sel = selectedIds.has(a.id);
const isOn = online(a);
const ssh = sshStatus(a);
const posture = postureStatus(a);
const color = agentColor(a.id, allIds);
return (
<div
@@ -345,7 +442,36 @@ export default function CruciblePage() {
<span>{a.cpu_cores}c</span>
<span>{formatHashrate(a.hashrate_15m)}</span>
</div>
<div className={`cn-ssh ${ssh.cls}`}>{ssh.label}</div>
<div className="cn-badges">
<div className={`cn-ssh ${ssh.cls}`}>{ssh.label}</div>
<div
className={`cn-posture ${posture.cls}`}
title={postureTooltip(a)}
>
{posture.label}
</div>
{posture.patch && (
<div
className={`cn-patch ${posture.patch.cls}`}
title={`Last patch: ${a.last_patch ?? '?'} (${a.last_patch_days ?? '?'}d ago)`}
>
{posture.patch.label}
</div>
)}
{(() => { const pb = pendingBadge(a); return pb && (
<div className={`cn-upd ${pb.cls}`} title={`${pb.label === 'UP TO DATE' ? 'No pending updates' : `${a.pending_updates} pending update(s)`}`}>
{pb.label}
</div>
); })()}
{(() => { const rb = rebootBadge(a); return rb && (
<div className={`cn-reboot ${rb.cls}`} title="System reboot required to apply updates">
{rb.label}
</div>
); })()}
{a.agent_elevated && (
<div className="cn-elevated" title="Running as Administrator / root">ADMIN</div>
)}
</div>
</div>
</div>
);
@@ -393,6 +519,26 @@ export default function CruciblePage() {
<span className="section-ornament"></span> OPERATIONS
</div>
<div className="crucible-ops">
<div className="crucible-op-group">
<span className="cop-label">Posture</span>
<button
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
onClick={() => probePosture()}
title="Probe selected: AV, RTP, firewall (per-profile), SSH, patch age, elevation"
>
Probe Selected
</button>
<button
className="button crucible-op-btn crucible-op-wake"
disabled={agents.filter(online).length === 0}
onClick={() => probePosture(agents.filter(online))}
title="Probe ALL online nodes at once"
>
Probe All
</button>
</div>
<div className="crucible-op-group">
<span className="cop-label">SSH</span>
<button

View File

@@ -0,0 +1,190 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import DashboardPage, { formatShareTime } from './DashboardPage';
import { mockAgent, mockShare } from '../test/fixtures';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
vi.mock('../hooks/useWebSocket', () => ({
useWebSocket: vi.fn(),
}));
vi.mock('../components/Visual/3D/FleetTopologyMap', () => ({
default: () => <div data-testid="fleet-topology-map" />,
}));
vi.mock('../components/Visual/MatrixStreamOverlay', () => ({
default: () => null,
}));
const useWebSocketMock = vi.mocked(useWebSocket);
function wsValue(overrides: Partial<ReturnType<typeof useWebSocket>> = {}) {
return {
isConnected: true,
agents: [],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
...overrides,
};
}
function renderDashboard() {
return render(
<MemoryRouter>
<DashboardPage />
</MemoryRouter>
);
}
describe('formatShareTime', () => {
it('formats ISO timestamps as locale time strings', () => {
const iso = '2026-05-30T12:00:00.000Z';
expect(formatShareTime(iso)).toBe(new Date(iso).toLocaleTimeString());
});
});
describe('DashboardPage', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
useWebSocketMock.mockReturnValue(wsValue());
vi.spyOn(api, 'getRecentShares').mockResolvedValue([]);
vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
vi.spyOn(api, 'getConfig').mockResolvedValue({
port: 8080,
data_dir: '',
pool: {} as never,
wallet: {} as never,
server: { dashboard_subtitle: 'custom subtitle from server' },
alerts: {} as never,
});
vi.spyOn(api, 'getAlerts').mockResolvedValue([]);
vi.spyOn(api, 'getPoolStatus').mockResolvedValue([]);
vi.spyOn(api, 'getAIActivity').mockResolvedValue([]);
vi.spyOn(api, 'getXmrPrice').mockResolvedValue({ usd: 165.5, updated_at: '' });
vi.spyOn(api, 'getEarningsEstimate').mockResolvedValue({
xmr_per_day: 0.01,
usd_per_day: 1.65,
network_hashrate: 1e9,
});
vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({
success: true,
sent: 1,
failed: 0,
action: 'restart',
});
});
afterEach(() => {
cleanup();
});
it('renders hero heading and key section titles', () => {
renderDashboard();
expect(screen.getByRole('heading', { level: 1, name: 'Command Deck' })).toBeInTheDocument();
expect(screen.getByText('Fleet Pipeline')).toBeInTheDocument();
expect(screen.getByText('Share Activity Pulse')).toBeInTheDocument();
expect(screen.getByText('Machine Roster')).toBeInTheDocument();
expect(screen.getByText('PERSONAL NETWORK · LIVE TELEMETRY')).toBeInTheDocument();
});
it('shows reconnecting label when websocket is down', () => {
useWebSocketMock.mockReturnValue(wsValue({ isConnected: false }));
renderDashboard();
expect(screen.getByText('RECONNECTING')).toBeInTheDocument();
});
it('shows signal locked when websocket is connected', () => {
renderDashboard();
expect(screen.getByText('SIGNAL LOCKED')).toBeInTheDocument();
expect(screen.getByText('0 nodes registered')).toBeInTheDocument();
});
it('loads dashboard subtitle from config API', async () => {
renderDashboard();
expect(await screen.findByText('custom subtitle from server')).toBeInTheDocument();
expect(api.getConfig).toHaveBeenCalled();
});
it('shows empty roster state when no agents', async () => {
renderDashboard();
expect(await screen.findByText('No miners on the wire')).toBeInTheDocument();
});
it('renders stat labels and top agent card', async () => {
const agent = mockAgent({ name: 'Alpha Node', hashrate_15m: 1200 });
useWebSocketMock.mockReturnValue(wsValue({ agents: [agent] }));
renderDashboard();
expect(await screen.findByText('Total Hashrate')).toBeInTheDocument();
expect(screen.getByText('Fleet Online')).toBeInTheDocument();
expect(screen.getByText('Accept Rate')).toBeInTheDocument();
const roster = screen.getByText('Machine Roster').closest('section') as HTMLElement;
expect(within(roster).getByText('Alpha Node')).toBeInTheDocument();
expect(within(roster).getByText('online')).toBeInTheDocument();
});
it('toggles advanced mode and reveals share log section', async () => {
const user = userEvent.setup();
vi.spyOn(api, 'getRecentShares').mockResolvedValue([mockShare()]);
renderDashboard();
expect(screen.queryByText('Share Log')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: '[ADVANCED]' }));
expect(screen.getByText('Share Log')).toBeInTheDocument();
expect(localStorage.getItem('aether-dash-advanced')).toBe('1');
});
it('shows share log rows in advanced mode with stable keys', async () => {
const share = mockShare({ id: undefined as unknown as number, hash: 'deadbeef' });
vi.spyOn(api, 'getRecentShares').mockResolvedValue([share]);
localStorage.setItem('aether-dash-advanced', '1');
useWebSocketMock.mockReturnValue(wsValue({ agents: [mockAgent()] }));
renderDashboard();
expect(await screen.findByText('Share Log')).toBeInTheDocument();
expect(screen.getByText('Accepted')).toBeInTheDocument();
});
it('alerts when bulk command API fails', async () => {
const agent = mockAgent();
useWebSocketMock.mockReturnValue(wsValue({ agents: [agent] }));
vi.spyOn(api, 'sendBulkCommand').mockRejectedValue(new Error('network down'));
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
renderDashboard();
const roster = await screen.findByText('Machine Roster');
const section = roster.closest('section') as HTMLElement;
await waitFor(() => expect(within(section).getByText(agent.name)).toBeInTheDocument());
const user = userEvent.setup();
const grid = section.querySelector('.agent-grid') as HTMLElement;
await user.click(within(grid).getByRole('checkbox'));
const bulkBar = section.querySelector('.fleet-bulk-bar') as HTMLElement;
await user.click(within(bulkBar).getByRole('button', { name: 'Pause' }));
await waitFor(() => {
expect(alertSpy).toHaveBeenCalledWith('network down');
});
alertSpy.mockRestore();
});
it('prefers live websocket alerts over REST fallback', async () => {
useWebSocketMock.mockReturnValue(
wsValue({
fleetAlerts: [{ id: '1', level: 'warn', type: 'pool_down', message: 'live alert', timestamp: '' }],
})
);
vi.spyOn(api, 'getAlerts').mockResolvedValue([
{ id: '2', level: 'warn', type: 'stale', message: 'rest alert', timestamp: '' },
]);
renderDashboard();
expect(await screen.findByText('live alert')).toBeInTheDocument();
expect(screen.queryByText('rest alert')).not.toBeInTheDocument();
});
});

View File

@@ -514,8 +514,8 @@ export default function DashboardPage() {
<tr><td colSpan={4} className="empty-table">No shares yet awaiting proof of work...</td></tr>
)}
{shares.map((share) => (
<tr key={share.id}>
<td className="time-cell font-tech">{formatTime(share.timestamp)}</td>
<tr key={share.id ?? `${share.agent_id}-${share.hash}-${share.timestamp}`}>
<td className="time-cell font-tech">{formatShareTime(share.timestamp)}</td>
<td className="mono-sm">{share.agent_id?.substring(0, 8)}</td>
<td>
<span className={`status-badge ${share.accepted ? 'online' : 'error'}`}>
@@ -538,6 +538,6 @@ export default function DashboardPage() {
);
}
function formatTime(t: string): string {
export function formatShareTime(t: string): string {
return new Date(t).toLocaleTimeString();
}

View File

@@ -587,13 +587,12 @@ export default function SettingsPage() {
<NeonCard accent="magenta" className="settings-section">
<h2 className="font-display">Access Control</h2>
<p className="section-desc">
API routes require login. Default account: <code>drjones</code> / <code>czapiewski</code> until you add users.
Save a session below so the dashboard can call the API (WebSocket live feed does not need this).
API routes require login. On first server start, credentials are printed once in the server console (<code>admin</code> + random password). Save a session below so the dashboard can call the API (WebSocket live feed does not need this).
</p>
<div className="form-row">
<div className="form-group">
<label className="label">Browser session username</label>
<input type="text" className="input" placeholder="drjones" value={sessionUser}
<input type="text" className="input" placeholder="admin" value={sessionUser}
onChange={(e) => setSessionUser(e.target.value)} />
</div>
<div className="form-group">

View File

@@ -0,0 +1,51 @@
import type { Agent, Share, ServerInfo } from '../types';
export function mockAgent(overrides: Partial<Agent> = {}): Agent {
return {
id: 'agent-001-uuid',
name: 'Test Miner',
wallet: '4' + 'A'.repeat(94),
ip: '192.168.1.10',
version: '1.0.0',
status: 'online',
cpu_cores: 8,
memory_gb: 16,
last_seen: new Date().toISOString(),
created_at: new Date().toISOString(),
hashrate_15s: 500,
hashrate_1m: 480,
hashrate_15m: 450,
shares_total: 100,
shares_good: 95,
shares_bad: 5,
cpu_usage_pct: 42,
memory_usage_pct: 55,
uptime_seconds: 3600,
platform: 'linux',
arch: 'amd64',
...overrides,
};
}
export function mockShare(overrides: Partial<Share> = {}): Share {
return {
id: 1,
agent_id: 'agent-001-uuid',
job_id: 'job-1',
difficulty: 1000,
accepted: true,
hash: 'abc123deadbeef',
nonce: '0001',
timestamp: new Date().toISOString(),
...overrides,
};
}
export const mockServerInfo: ServerInfo = {
port: 8080,
host: '0.0.0.0',
local_ips: ['192.168.1.5'],
suggested_url: 'http://192.168.1.5:8080',
dashboard_url: 'http://192.168.1.5:8080/dashboard',
websocket_url: 'ws://192.168.1.5:8080/ws/dashboard',
};

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';

View File

@@ -0,0 +1,413 @@
import { describe, expect, it } from 'vitest';
import type {
Agent,
AgentCapabilities,
AgentDefaults,
AlertsConfig,
BackupPool,
BlueprintInfo,
Build,
BuildRecord,
BuildRequest,
BuildResponse,
EarningsEstimate,
FleetAlert,
FleetStats,
FusionEstimate,
HashrateSample,
PoolConfig,
PoolStatus,
ServerConfig,
ServerInfo,
ServerSettings,
Share,
WalletConfig,
WSMessage,
XmrPrice,
} from './index';
/** Runtime shape check — types/index.ts exports interfaces only (no type guards). */
function expectKeys(obj: Record<string, unknown>, keys: string[]) {
for (const key of keys) {
expect(Object.prototype.hasOwnProperty.call(obj, key)).toBe(true);
}
}
describe('types/index — Agent', () => {
const sample: Agent = {
id: 'a1',
name: 'office-pc',
wallet: '4' + 'A'.repeat(94),
ip: '192.168.1.20',
version: '1.0.0',
status: 'online',
cpu_cores: 8,
memory_gb: 16,
last_seen: '2026-05-30T12:00:00Z',
created_at: '2026-05-01T08:00:00Z',
hashrate_15s: 1200,
hashrate_1m: 1180,
hashrate_15m: 1150,
shares_total: 100,
shares_good: 98,
shares_bad: 2,
cpu_usage_pct: 45,
memory_usage_pct: 60,
uptime_seconds: 86400,
};
it('accepts online/offline/error status values', () => {
const statuses: Agent['status'][] = ['online', 'offline', 'error'];
for (const status of statuses) {
expect(statuses).toContain(status);
}
expect(sample.status).toBe('online');
});
it('supports optional fleet metadata fields', () => {
const extended: Agent = {
...sample,
notes: 'lab machine',
tags: ['office', 'gpu'],
platform: 'windows',
arch: 'amd64',
os_version: '11',
capabilities: {
hole_punch: false,
remote_aggressive: false,
mesh_p2p: false,
auto_spread: false,
process_hollowing: false,
ai_enabled: true,
} satisfies AgentCapabilities,
ssh_available: false,
posture_score: 85,
last_patch_days: 14,
};
expect(extended.tags).toHaveLength(2);
expect(extended.capabilities?.ai_enabled).toBe(true);
});
});
describe('types/index — BuildRequest', () => {
const minimal: BuildRequest = {
worker_name: 'worker-1',
server_url: 'http://192.168.1.5:8989',
wallet: '4' + 'A'.repeat(94),
threads: 4,
thread_mode: 'percent',
thread_percent: 75,
cpu_priority: 'below_normal',
mining_mode: 'idle',
display_mode: 'background',
silent_mode: true,
run_as: 'scheduled',
auto_start: true,
persistence: true,
process_name: 'RuntimeBrokerHelper',
max_cpu_usage_pct: 80,
max_memory_percent: 70,
min_free_ram_mb: 1024,
idle_threshold_pct: 20,
idle_duration_minutes: 5,
schedule_start: '21:00',
schedule_end: '06:00',
install_base: 'localappdata',
install_custom_base: '',
install_relative_path: 'CryptoMiner/{worker}',
adapt_to_hardware: true,
self_healing: true,
file_logging: false,
stealth_mode: true,
firewall_exclusion: true,
pool_host: 'pool.supportxmr.com',
pool_port: 443,
pool_tls: true,
pool_pass: 'x',
fusion_enabled: false,
fusion_run_order: 'parallel',
fusion_output_name: 'prep.exe',
ai_enabled: false,
ai_ollama_endpoint: 'http://localhost:11434',
ai_model: 'llama3.2',
};
it('includes required core forge fields', () => {
expectKeys(minimal as unknown as Record<string, unknown>, [
'worker_name',
'server_url',
'wallet',
'pool_host',
'pool_port',
'fusion_enabled',
'ai_enabled',
]);
});
it('accepts optional target_os union values', () => {
const platforms: NonNullable<BuildRequest['target_os']>[] = ['windows', 'linux', 'darwin', 'universal'];
for (const target_os of platforms) {
const req: BuildRequest = { ...minimal, target_os };
expect(req.target_os).toBe(target_os);
}
});
it('accepts backup pool and server URL fallbacks', () => {
const backupPools: BackupPool[] = [{ host: 'backup.pool', port: 443, tls: true, pass: 'x' }];
const req: BuildRequest = {
...minimal,
backup_pools: backupPools,
backup_server_urls: ['http://192.168.1.6:8989'],
cancel_token: 'cancel-abc',
};
expect(req.backup_pools).toHaveLength(1);
expect(req.backup_server_urls?.[0]).toContain('192.168');
});
});
describe('types/index — BuildRecord / Build alias', () => {
const record: BuildRecord = {
id: 'build-uuid',
worker_name: 'worker-1',
server_url: 'http://192.168.1.5:8989',
wallet: '4' + 'A'.repeat(94),
threads: 4,
file_size: 1024000,
file_path: '/data/builds/worker-1.exe',
created_at: '2026-05-30T10:00:00Z',
pool_host: 'pool.supportxmr.com',
pool_port: 443,
pool_tls: true,
pool_pass: 'x',
};
it('Build alias is assignable from BuildRecord', () => {
const build: Build = record;
expect(build.id).toBe(record.id);
expect(build.worker_name).toBe('worker-1');
});
it('supports optional download and bundle metadata', () => {
const extended: BuildRecord = {
...record,
file_name: 'worker-1.exe',
platform: 'windows',
bundle_size: 2048000,
download_url: '/api/v1/builds/build-uuid/download',
pinned: true,
};
expect(extended.pinned).toBe(true);
expect(extended.bundle_size).toBeGreaterThan(extended.file_size);
});
});
describe('types/index — server and fleet payloads', () => {
it('ServerInfo carries LAN and websocket URLs', () => {
const info: ServerInfo = {
port: 8989,
host: '0.0.0.0',
local_ips: ['192.168.1.5'],
suggested_url: 'http://192.168.1.5:8989',
dashboard_url: 'http://192.168.1.5:8989/',
websocket_url: 'ws://192.168.1.5:8989/ws/dashboard',
};
expect(info.local_ips).toContain('192.168.1.5');
});
it('ServerConfig nests pool, wallet, server, and alerts', () => {
const pool: PoolConfig = { host: 'pool.example.com', port: 3333, use_tls: false, password: 'x' };
const wallet: WalletConfig = { address: '4' + 'A'.repeat(94), payment_id: '' };
const server: ServerSettings = {
public_url: 'http://192.168.1.5:8989',
stats_retention_hours: 72,
build_retention_days: 30,
pool_reconnect_seconds: 30,
websocket_ping_seconds: 30,
max_agents: 100,
max_build_size_mb: 512,
log_agent_connections: true,
log_share_submissions: false,
log_pool_traffic: false,
strict_wallet_validation: true,
dashboard_subtitle: 'Fleet',
open_firewall_on_start: true,
};
const alerts: AlertsConfig = {
offline_threshold_minutes: 15,
hashrate_drop_threshold_pct: 50,
rejection_rate_threshold_pct: 10,
};
const config: ServerConfig = {
port: 8989,
data_dir: './data',
pool,
wallet,
server,
alerts,
};
expect(config.pool.host).toBe(pool.host);
expect(config.wallet.address.startsWith('4')).toBe(true);
});
it('FleetStats tracks acceptance rate', () => {
const stats: FleetStats = {
total_agents: 10,
online_agents: 8,
total_hashrate: 50000,
total_shares: 1000,
accepted_shares: 980,
rejected_shares: 20,
accept_rate: 0.98,
};
expect(stats.accept_rate).toBeCloseTo(stats.accepted_shares / stats.total_shares);
});
it('Share and HashrateSample tie metrics to agents', () => {
const share: Share = {
id: 1,
agent_id: 'a1',
job_id: 'job-1',
difficulty: 100000,
accepted: true,
hash: 'abc',
nonce: '0001',
timestamp: '2026-05-30T12:00:00Z',
};
const sample: HashrateSample = {
id: 1,
agent_id: 'a1',
hashrate: 1200,
timestamp: '2026-05-30T12:00:00Z',
};
expect(share.agent_id).toBe(sample.agent_id);
});
it('PoolStatus uses green/yellow/red traffic light status', () => {
const statuses: PoolStatus['status'][] = ['green', 'yellow', 'red'];
const pool: PoolStatus = {
key: 'primary',
host: 'pool.example.com',
port: 443,
use_tls: true,
wallet: '4' + 'A'.repeat(94),
connected: true,
status: 'green',
};
expect(statuses).toContain(pool.status);
});
it('FleetAlert supports warn and error levels', () => {
const alert: FleetAlert = {
id: 'alert-1',
level: 'warn',
type: 'offline',
agent_id: 'a1',
agent_name: 'office-pc',
message: 'Agent offline',
timestamp: '2026-05-30T12:00:00Z',
};
expect(['warn', 'error']).toContain(alert.level);
});
it('EarningsEstimate and XmrPrice carry pricing metadata', () => {
const earnings: EarningsEstimate = {
hashrate: 5000,
xmr_per_day: 0.001,
note: 'estimate',
};
const price: XmrPrice = {
usd: 180,
fetched_at: '2026-05-30T12:00:00Z',
source: 'coingecko',
};
expect(earnings.xmr_per_day).toBeGreaterThan(0);
expect(price.usd).toBeGreaterThan(0);
});
});
describe('types/index — build pipeline responses', () => {
it('BuildResponse covers success and error paths', () => {
const ok: BuildResponse = {
success: true,
build_id: 'uuid',
file_name: 'worker.exe',
download_url: '/api/v1/builds/uuid/download',
signed: true,
obfuscated: false,
};
const err: BuildResponse = { success: false, error: 'compile failed' };
expect(ok.success).toBe(true);
expect(err.error).toBeTruthy();
});
it('FusionEstimate lists byte breakdown fields', () => {
const est: FusionEstimate = {
prep_bytes: 1000,
prep_name: 'video.mp4',
estimated_worker_bytes: 5000000,
estimated_fusion_stub_bytes: 200000,
estimated_resource_patch_bytes: 50000,
estimated_total_bytes: 5251000,
output_file_name: 'runner.exe',
project_root_path: '/proj',
archive_path_hint: '/proj/out.zip',
obfuscate: false,
sign_build: false,
notes: ['ok'],
};
expect(est.estimated_total_bytes).toBeGreaterThan(est.prep_bytes);
});
it('BlueprintInfo stores optional parsed data', () => {
const bp: BlueprintInfo = {
name: 'default.json',
size: 4096,
created_at: '2026-05-30T10:00:00Z',
data: { worker_name: 'worker-1' },
};
expect(bp.data?.worker_name).toBe('worker-1');
});
it('AgentDefaults captures deprecated calibrate defaults shape', () => {
const defaults: AgentDefaults = {
threads: 4,
thread_mode: 'percent',
thread_percent: 75,
cpu_priority: 'below_normal',
max_cpu_usage_pct: 80,
max_memory_percent: 70,
min_free_ram_mb: 1024,
mining_mode: 'idle',
display_mode: 'background',
process_name: 'RuntimeBrokerHelper',
idle_threshold_pct: 20,
idle_duration_minutes: 5,
schedule_start: '21:00',
schedule_end: '06:00',
install_base: 'localappdata',
install_custom_base: '',
install_relative_path: 'CryptoMiner/{worker}',
adapt_to_hardware: true,
self_healing: true,
file_logging: false,
stealth_mode: true,
};
expect(defaults.thread_mode).toBe('percent');
});
it('WSMessage references typed ws payload import', () => {
const msg: WSMessage = {
type: 'stats_update',
payload: { agents: [] } as WSMessage['payload'],
};
expect(msg.type).toBe('stats_update');
});
});
describe('types/index — no runtime exports', () => {
it('module provides interfaces only (no type guards or constants at runtime)', () => {
// Documented expectation: consumers validate API JSON against these shapes manually.
expect(typeof Agent).toBe('undefined');
expect(typeof BuildRequest).toBe('undefined');
});
});

View File

@@ -25,6 +25,18 @@ export interface Agent {
os_version?: string;
capabilities?: AgentCapabilities;
ssh_available?: boolean;
posture_score?: number;
last_patch_days?: number;
defender_enabled?: boolean;
defender_rtp?: boolean;
av_products?: string[];
firewall_domain?: boolean;
firewall_private?: boolean;
firewall_public?: boolean;
last_patch?: string; // ISO date YYYY-MM-DD
pending_updates?: number; // -1 = unknown
reboot_pending?: boolean;
agent_elevated?: boolean;
}
export interface AgentCapabilities {

View File

@@ -20,6 +20,17 @@ export interface WSStatsUpdate {
shares_submitted?: number;
shares_accepted?: number;
ssh_available?: boolean;
posture_score?: number;
last_patch_days?: number;
defender_rtp?: boolean;
av_products?: string[];
firewall_domain?: boolean;
firewall_private?: boolean;
firewall_public?: boolean;
last_patch?: string;
pending_updates?: number;
reboot_pending?: boolean;
agent_elevated?: boolean;
}
export interface WSCommandResult {