Update server config, builder APK logic, frontend fleet/activity metrics, and ignore test APKs
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-13 19:54:24 -07:00
parent df88d160cb
commit 689e574f7a
17 changed files with 497 additions and 65 deletions

View File

@@ -118,6 +118,10 @@ export default function CrucibleExpandedOps({
}
}, [singleSelectedAgent?.mac_address, wolMac]);
useEffect(() => {
setLiveDesktop(false);
}, [singleSelectedAgent?.id]);
const dispatchOne = useCallback(
async (agent: Agent, action: string, args: Record<string, unknown> = {}) => {
try {

View File

@@ -37,4 +37,21 @@ describe('fleetGroups', () => {
saveFleetGroups(groups);
expect(loadFleetGroups()).toHaveLength(2);
});
it('generates unique fallback IDs for loaded groups missing an ID', () => {
// Manually saving raw JSON objects without IDs to simulate legacy state
const rawGroups = [
{ name: 'Legacy Group 1', color: '#ff0000', agentIds: [] },
{ name: 'Legacy Group 2', color: '#00ff00', agentIds: [] },
];
localStorage.setItem('aetherforge_fleet_groups', JSON.stringify(rawGroups));
const loaded = loadFleetGroups();
expect(loaded).toHaveLength(2);
expect(loaded[0].id).toBeDefined();
expect(loaded[1].id).toBeDefined();
expect(loaded[0].id).not.toBe(loaded[1].id);
expect(loaded[0].id.startsWith('fg-')).toBe(true);
expect(loaded[1].id.startsWith('fg-')).toBe(true);
});
});

View File

@@ -54,7 +54,7 @@ export function loadFleetGroups(): FleetGroup[] {
? [...new Set(o.agentIds.filter((id): id is string => typeof id === 'string' && id.length > 0))]
: [];
return {
id: typeof o.id === 'string' && o.id ? o.id : `fg-${Date.now()}`,
id: typeof o.id === 'string' && o.id ? o.id : `fg-${crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`}`,
name,
color: normalizeGroupColor(typeof o.color === 'string' ? o.color : FLEET_GROUP_COLORS[0]),
agentIds,

View File

@@ -138,6 +138,112 @@ describe('agentStatsUnchanged', () => {
}),
).toBe(false);
});
it('returns true when failed_methods, services, atlas_skips, and vuln_findings are structurally identical but have different array references', () => {
const agent = mockAgent({
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
failed_methods: [{ method: 'container', reason: 'blocked', at: '2026-06-06T12:00:00Z' }],
services: [{ name: 'Wuauserv', display_name: 'Windows Update', status: 'running', start_type: 'auto' }],
atlas_skips: [{ tier: 'smb', condition: 'isolated', reason: 'timeout' }],
vuln_findings: [{ cve_id: 'CVE-2021-26855', severity: 'critical', patched: false }],
});
expect(
agentStatsUnchanged(agent, {
agent_id: agent.id,
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
failed_methods: [{ method: 'container', reason: 'blocked', at: '2026-06-06T12:00:00Z' }],
services: [{ name: 'Wuauserv', display_name: 'Windows Update', status: 'running', start_type: 'auto' }],
atlas_skips: [{ tier: 'smb', condition: 'isolated', reason: 'timeout' }],
vuln_findings: [{ cve_id: 'CVE-2021-26855', severity: 'critical', patched: false }],
}),
).toBe(true);
});
it('returns false when failed_methods change', () => {
const agent = mockAgent({
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
failed_methods: [{ method: 'container', reason: 'blocked', at: '2026-06-06T12:00:00Z' }],
});
expect(
agentStatsUnchanged(agent, {
agent_id: agent.id,
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
failed_methods: [{ method: 'container', reason: 'AV blocked', at: '2026-06-06T12:00:00Z' }],
}),
).toBe(false);
});
it('returns false when services change', () => {
const agent = mockAgent({
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
services: [{ name: 'Wuauserv', display_name: 'Windows Update', status: 'running', start_type: 'auto' }],
});
expect(
agentStatsUnchanged(agent, {
agent_id: agent.id,
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
services: [{ name: 'Wuauserv', display_name: 'Windows Update', status: 'stopped', start_type: 'auto' }],
}),
).toBe(false);
});
it('returns false when atlas_skips change', () => {
const agent = mockAgent({
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
atlas_skips: [{ tier: 'smb', condition: 'isolated', reason: 'timeout' }],
});
expect(
agentStatsUnchanged(agent, {
agent_id: agent.id,
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
atlas_skips: [{ tier: 'smb', condition: 'isolated', reason: 'failed-5-times' }],
}),
).toBe(false);
});
it('returns false when vuln_findings change', () => {
const agent = mockAgent({
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
vuln_findings: [{ cve_id: 'CVE-2021-26855', severity: 'critical', patched: false }],
});
expect(
agentStatsUnchanged(agent, {
agent_id: agent.id,
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
vuln_findings: [{ cve_id: 'CVE-2021-26855', severity: 'critical', patched: true }],
}),
).toBe(false);
});
});
describe('WS_LATEST_MESSAGE_TYPES', () => {

View File

@@ -1,4 +1,4 @@
import type { Agent } from '../types';
import type { Agent, AgentService } from '../types';
import type { WSStatsUpdate } from '../types/ws';
/** Returns true when a stats_update payload would not change visible agent fields. */
@@ -44,16 +44,17 @@ export function agentStatsUnchanged(agent: Agent, u: WSStatsUpdate): boolean {
if (u.stratum_overlay !== undefined && agent.stratum_overlay !== u.stratum_overlay) return false;
if (u.chain_exhausted !== undefined && agent.chain_exhausted !== u.chain_exhausted) return false;
if (u.chain_order !== undefined && !shallowStrArrayEq(agent.chain_order, u.chain_order)) return false;
if (u.failed_methods !== undefined && agent.failed_methods !== u.failed_methods) return false;
if (u.failed_methods !== undefined && !failedMethodsEq(agent.failed_methods, u.failed_methods)) return false;
if (u.dns_servers !== undefined && !shallowStrArrayEq(agent.dns_servers, u.dns_servers)) return false;
if (u.dns_search_domains !== undefined && !shallowStrArrayEq(agent.dns_search_domains, u.dns_search_domains)) return false;
if (u.av_products !== undefined && !shallowStrArrayEq(agent.av_products, u.av_products)) return false;
if (u.services !== undefined && agent.services !== u.services) return false;
if (u.services !== undefined && !servicesEq(agent.services, u.services)) return false;
if (u.mining_hashrate !== undefined && agent.mining_hashrate !== u.mining_hashrate) return false;
if (u.mining_block_reason !== undefined && agent.mining_block_reason !== u.mining_block_reason) return false;
if (u.lotl_tier !== undefined && agent.lotl_tier !== u.lotl_tier) return false;
if (u.lotl_attempts !== undefined && !tierAttemptsEq(agent.lotl_attempts, u.lotl_attempts)) return false;
if (u.vuln_findings !== undefined && agent.vuln_findings !== u.vuln_findings) return false;
if (u.atlas_skips !== undefined && !atlasSkipsEq(agent.atlas_skips, u.atlas_skips)) return false;
if (u.vuln_findings !== undefined && !vulnFindingsEq(agent.vuln_findings, u.vuln_findings)) return false;
if (u.vuln_risk_score !== undefined && agent.vuln_risk_score !== u.vuln_risk_score) return false;
if (u.join_lane !== undefined && agent.join_lane !== u.join_lane) return false;
if (u.parent_agent_id !== undefined && agent.parent_agent_id !== u.parent_agent_id) return false;
@@ -84,6 +85,74 @@ function tierAttemptsEq(a?: import('../types/lotl').TierAttempt[], b?: import('.
return true;
}
function failedMethodsEq(
a?: { method: string; reason: string; at: string }[],
b?: { method: string; reason: string; at: string }[]
): boolean {
if (a === b) return true;
if (!a || !b || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].method !== b[i].method || a[i].reason !== b[i].reason || a[i].at !== b[i].at) {
return false;
}
}
return true;
}
function servicesEq(a?: AgentService[], b?: AgentService[]): boolean {
if (a === b) return true;
if (!a || !b || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
const x = a[i];
const y = b[i];
if (
x.name !== y.name ||
x.display_name !== y.display_name ||
x.status !== y.status ||
x.start_type !== y.start_type
) {
return false;
}
}
return true;
}
function atlasSkipsEq(
a?: { tier: string; condition: string; reason: string }[],
b?: { tier: string; condition: string; reason: string }[]
): boolean {
if (a === b) return true;
if (!a || !b || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].tier !== b[i].tier || a[i].condition !== b[i].condition || a[i].reason !== b[i].reason) {
return false;
}
}
return true;
}
function vulnFindingsEq(
a?: import('../types/recon').VulnFinding[],
b?: import('../types/recon').VulnFinding[]
): boolean {
if (a === b) return true;
if (!a || !b || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
const x = a[i];
const y = b[i];
if (
x.cve_id !== y.cve_id ||
x.severity !== y.severity ||
x.component !== y.component ||
x.patched !== y.patched ||
x.exploitable_in_fleet_context !== y.exploitable_in_fleet_context
) {
return false;
}
}
return true;
}
/** WS message types that drive latestMessage consumers (sound, presence, emberwake). */
export const WS_LATEST_MESSAGE_TYPES = new Set([
'presence_snapshot',

View File

@@ -103,6 +103,9 @@ export default function ActivityFeedPage() {
const prevAgentStatus = useRef<Record<string, string>>({}); // id → status
const prevHashrates = useRef<Record<string, number>>({}); // id → hashrate_15m
const prevPosture = useRef<Record<string, number>>({}); // id → posture_score
const seenShares = useRef<Set<string>>(new Set());
const seenAlerts = useRef<Set<string>>(new Set());
const aiInitialized = useRef(false);
// Build agent name lookup
useEffect(() => {
@@ -160,7 +163,7 @@ export default function ActivityFeedPage() {
const prev = prevHashrates.current[agent.id];
const cur = agent.hashrate_15m ?? 0;
prevHashrates.current[agent.id] = cur;
if (prev === undefined || prev <= 0) continue;
if (prev === undefined) continue;
const delta = cur - prev;
// Only emit if ≥20% change AND at least 100 H/s delta
if (Math.abs(delta) >= 100 && Math.abs(delta) / Math.max(prev, 1) >= 0.20) {
@@ -197,59 +200,137 @@ export default function ActivityFeedPage() {
}, [agents, push]);
// ── New share events ───────────────────────────────────────────────────
const lastShareId = useRef<string | null>(null);
useEffect(() => {
if (recentShares.length === 0) return;
const top = recentShares[0];
const key = top.id != null ? String(top.id) : `${top.agent_id}-${top.hash}`;
if (key === lastShareId.current) return;
lastShareId.current = key;
const name = agentMapRef.current.get(top.agent_id) ?? top.agent_id?.slice(0, 8);
push({
id: eid(), kind: 'share',
agentId: top.agent_id, agentName: name,
message: top.accepted ? 'share accepted by pool' : 'share rejected',
detail: top.accepted ? undefined : top.error ?? 'pool rejection',
ts: new Date(top.timestamp ?? Date.now()),
});
// On first load, we initialize the seen list to avoid back-filling old shares
if (seenShares.current.size === 0) {
for (const s of recentShares) {
const key = s.id != null ? String(s.id) : `${s.agent_id}-${s.hash}`;
seenShares.current.add(key);
}
return;
}
const newShares = [];
for (let i = recentShares.length - 1; i >= 0; i--) {
const s = recentShares[i];
const key = s.id != null ? String(s.id) : `${s.agent_id}-${s.hash}`;
if (!seenShares.current.has(key)) {
seenShares.current.add(key);
newShares.push(s);
}
}
if (seenShares.current.size > 200) {
const nextSet = new Set<string>();
for (const s of recentShares) {
const key = s.id != null ? String(s.id) : `${s.agent_id}-${s.hash}`;
nextSet.add(key);
}
seenShares.current = nextSet;
}
for (const top of newShares) {
const name = agentMapRef.current.get(top.agent_id) ?? top.agent_id?.slice(0, 8);
push({
id: eid(), kind: 'share',
agentId: top.agent_id, agentName: name,
message: top.accepted ? 'share accepted by pool' : 'share rejected',
detail: top.accepted ? undefined : top.error ?? 'pool rejection',
ts: new Date(top.timestamp ?? Date.now()),
});
}
}, [recentShares, push]);
// ── Fleet alert events ─────────────────────────────────────────────────
const lastAlertId = useRef<string | null>(null);
useEffect(() => {
if (fleetAlerts.length === 0) return;
const top = fleetAlerts[0];
if (top.id === lastAlertId.current) return;
lastAlertId.current = top.id;
push({
id: eid(), kind: 'alert',
agentId: top.agent_id, agentName: top.agent_name,
message: top.message,
detail: top.type,
ts: new Date(top.timestamp ?? Date.now()),
});
if (seenAlerts.current.size === 0) {
for (const a of fleetAlerts) {
seenAlerts.current.add(a.id);
}
return;
}
const newAlerts = [];
for (let i = fleetAlerts.length - 1; i >= 0; i--) {
const a = fleetAlerts[i];
if (!seenAlerts.current.has(a.id)) {
seenAlerts.current.add(a.id);
newAlerts.push(a);
}
}
if (seenAlerts.current.size > 200) {
const nextSet = new Set<string>();
for (const a of fleetAlerts) {
nextSet.add(a.id);
}
seenAlerts.current = nextSet;
}
for (const top of newAlerts) {
push({
id: eid(), kind: 'alert',
agentId: top.agent_id, agentName: top.agent_name,
message: top.message,
detail: top.type,
ts: new Date(top.timestamp ?? Date.now()),
});
}
}, [fleetAlerts, push]);
// ── Command result events ──────────────────────────────────────────────
const lastCmdSeq = useRef(-1);
useEffect(() => {
if (commandResults.length === 0) return;
const top = commandResults[commandResults.length - 1];
if ((top._seq ?? -1) <= lastCmdSeq.current) return;
lastCmdSeq.current = top._seq ?? -1;
const name = agentMapRef.current.get(top.agent_id ?? '') ?? top.agent_id?.slice(0, 8);
push({
id: eid(), kind: 'command',
agentId: top.agent_id, agentName: name,
message: `${top.action}${top.success ? 'success' : 'failed'}`,
detail: top.success ? undefined : top.message?.slice(0, 80),
ts: new Date(),
});
if (lastCmdSeq.current === -1) {
lastCmdSeq.current = Math.max(...commandResults.map((r) => r._seq ?? -1));
return;
}
const newResults = [];
for (const r of commandResults) {
const seq = r._seq ?? -1;
if (seq > lastCmdSeq.current) {
newResults.push(r);
}
}
if (newResults.length > 0) {
lastCmdSeq.current = Math.max(...newResults.map((r) => r._seq ?? -1));
}
for (const top of newResults) {
const name = agentMapRef.current.get(top.agent_id ?? '') ?? top.agent_id?.slice(0, 8);
push({
id: eid(), kind: 'command',
agentId: top.agent_id, agentName: name,
message: `${top.action}${top.success ? 'success' : 'failed'}`,
detail: top.success ? undefined : top.message?.slice(0, 80),
ts: new Date(),
});
}
}, [commandResults, push]);
// ── AI activity events ─────────────────────────────────────────────────
const lastAiAgent = useRef<Record<string, string>>({});
useEffect(() => {
if (aiActivity.length === 0) return;
if (!aiInitialized.current) {
for (const entry of aiActivity) {
if (entry.last_action) {
lastAiAgent.current[entry.agent_id] = entry.last_action;
}
}
aiInitialized.current = true;
return;
}
for (const entry of aiActivity) {
const lastAction = lastAiAgent.current[entry.agent_id];
if (entry.last_action && entry.last_action !== lastAction) {

View File

@@ -108,7 +108,7 @@ export default function ROIPage() {
? (hr / totalHashrate) * xmrPerDay
: 0;
const nodeUsdDay = nodeXmrDay * price;
const nodeCores = a.cpu_cores ?? 0;
const nodeCores = a.status === 'online' ? (a.cpu_cores ?? 0) : 0;
const nodeWatts = nodeCores * WATT_PER_CORE_ESTIMATE;
const nodeKwhDay = (nodeWatts / 1000) * 24;
const nodeElecCost = nodeKwhDay * kwh;