fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes
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.
This commit is contained in:
@@ -5,8 +5,6 @@ import {
|
||||
resolveChartSeries,
|
||||
chartSeriesDelta,
|
||||
chartSeriesPeak,
|
||||
SAMPLE_CONTRIBUTION_BARS,
|
||||
SAMPLE_FLEET_PREVIEW,
|
||||
} from './chartSampleData';
|
||||
|
||||
describe('chartSampleData', () => {
|
||||
@@ -20,44 +18,23 @@ describe('chartSampleData', () => {
|
||||
expect(chartSeriesPeak(series)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('hashrate sample trends upward (mining ramp)', () => {
|
||||
const series = generateSampleSeries('hashrate', 48);
|
||||
expect(series[series.length - 1].value).toBeGreaterThan(series[0].value);
|
||||
const delta = chartSeriesDelta(series);
|
||||
expect(delta).not.toBeNull();
|
||||
expect(delta!).toBeGreaterThan(0);
|
||||
it('resolveChartSeries returns empty when live is empty', () => {
|
||||
const { data, mode } = resolveChartSeries([]);
|
||||
expect(mode).toBe('empty');
|
||||
expect(data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('accept sample stays in realistic pool band', () => {
|
||||
const series = generateSampleSeries('accept', 48);
|
||||
for (const p of series) {
|
||||
expect(p.value).toBeGreaterThanOrEqual(90);
|
||||
expect(p.value).toBeLessThanOrEqual(100);
|
||||
}
|
||||
});
|
||||
|
||||
it('resolveChartSeries uses sample when live is empty', () => {
|
||||
const { data, mode } = resolveChartSeries([], 'hashrate');
|
||||
expect(mode).toBe('sample');
|
||||
expect(data.length).toBe(48);
|
||||
expect(validateChartSeries(data).ok).toBe(true);
|
||||
});
|
||||
|
||||
it('resolveChartSeries prefers live when enough points', () => {
|
||||
const live = generateSampleSeries('cpu', 20).map((p, i) => ({
|
||||
...p,
|
||||
value: 40 + i * 0.5,
|
||||
}));
|
||||
const { data, mode } = resolveChartSeries(live, 'cpu');
|
||||
it('resolveChartSeries returns live slice when data exists', () => {
|
||||
const live = generateSampleSeries('cpu', 20);
|
||||
const { data, mode } = resolveChartSeries(live);
|
||||
expect(mode).toBe('live');
|
||||
expect(data.length).toBe(20);
|
||||
});
|
||||
|
||||
it('preview constants are internally consistent', () => {
|
||||
const totalPct = SAMPLE_CONTRIBUTION_BARS.reduce((s, b) => s + b.pct, 0);
|
||||
expect(totalPct).toBeGreaterThan(98);
|
||||
expect(totalPct).toBeLessThan(102);
|
||||
expect(SAMPLE_FLEET_PREVIEW.hashrate).toBeGreaterThan(50_000);
|
||||
expect(SAMPLE_FLEET_PREVIEW.xmrPerDay).toBeGreaterThan(0);
|
||||
it('chartSeriesDelta computes trend', () => {
|
||||
const series = generateSampleSeries('hashrate', 48);
|
||||
const delta = chartSeriesDelta(series);
|
||||
expect(delta).not.toBeNull();
|
||||
expect(delta!).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,41 +1,8 @@
|
||||
import type { ChartPoint } from '../components/Charts/HashrateChart';
|
||||
import type { ContributionBar } from './fleetAnalytics';
|
||||
|
||||
export type ChartSeriesKind = 'hashrate' | 'accept' | 'cpu' | 'mem' | 'gpu';
|
||||
|
||||
export type ChartDisplayMode = 'live' | 'sample' | 'blend';
|
||||
|
||||
const MIN_LIVE_POINTS = 12;
|
||||
|
||||
/** Fleet snapshot shown when no live miners — deck still feels “about to print”. */
|
||||
export const SAMPLE_FLEET_PREVIEW = {
|
||||
hashrate: 128_400,
|
||||
acceptRate: 96.8,
|
||||
avgCpu: 52,
|
||||
avgMem: 61,
|
||||
onlinePct: 88,
|
||||
onlineCount: 7,
|
||||
agentCount: 8,
|
||||
xmrPerDay: 0.0384,
|
||||
xmrPrice: 168.42,
|
||||
} as const;
|
||||
|
||||
export const SAMPLE_CONTRIBUTION_BARS: ContributionBar[] = [
|
||||
{ id: 's1', name: 'Vault-01', hashrate: 42_800, pct: 33.4 },
|
||||
{ id: 's2', name: 'Forge-Rig', hashrate: 31_200, pct: 24.3 },
|
||||
{ id: 's3', name: 'Lan-Node-7', hashrate: 28_100, pct: 21.9 },
|
||||
{ id: 's4', name: 'Basement-XMR', hashrate: 26_300, pct: 20.4 },
|
||||
];
|
||||
|
||||
export const SAMPLE_ACTIVITY = [
|
||||
{ id: 'sa1', label: 'OK', ok: true, time: '12:04:11' },
|
||||
{ id: 'sa2', label: 'OK', ok: true, time: '12:03:58' },
|
||||
{ id: 'sa3', label: 'OK', ok: true, time: '12:03:41' },
|
||||
{ id: 'sa4', label: 'OK', ok: true, time: '12:03:22' },
|
||||
{ id: 'sa5', label: 'OK', ok: true, time: '12:02:59' },
|
||||
{ id: 'sa6', label: 'BAD', ok: false, time: '12:02:44' },
|
||||
{ id: 'sa7', label: 'OK', ok: true, time: '12:02:31' },
|
||||
];
|
||||
export type ChartDisplayMode = 'live' | 'empty';
|
||||
|
||||
function formatTime(offsetMin: number): string {
|
||||
const d = new Date(Date.now() - offsetMin * 60_000);
|
||||
@@ -46,7 +13,7 @@ function noise(i: number, amp: number): number {
|
||||
return Math.sin(i * 0.7) * amp + Math.cos(i * 0.31) * (amp * 0.6);
|
||||
}
|
||||
|
||||
/** Deterministic rich-looking telemetry for chart QA and empty-deck preview. */
|
||||
/** Test-only synthetic series (not used in production UI). */
|
||||
export function generateSampleSeries(kind: ChartSeriesKind, points = 48): ChartPoint[] {
|
||||
const out: ChartPoint[] = [];
|
||||
for (let i = points - 1; i >= 0; i--) {
|
||||
@@ -92,41 +59,13 @@ export function validateChartSeries(data: ChartPoint[]): { ok: boolean; errors:
|
||||
return { ok: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
function hasMeaningfulLive(live: ChartPoint[], kind: ChartSeriesKind): boolean {
|
||||
if (live.length < MIN_LIVE_POINTS) return false;
|
||||
const vals = live.map((p) => p.value);
|
||||
const max = Math.max(...vals);
|
||||
const min = Math.min(...vals);
|
||||
if (kind === 'hashrate' || kind === 'gpu') return max > 0 && max !== min;
|
||||
return max - min > 0.05;
|
||||
}
|
||||
|
||||
/** Prefer live telemetry; pad with sample so graphs never look broken or empty. */
|
||||
export function resolveChartSeries(
|
||||
live: ChartPoint[],
|
||||
kind: ChartSeriesKind,
|
||||
options?: { tailValue?: number; minPoints?: number }
|
||||
): { data: ChartPoint[]; mode: ChartDisplayMode } {
|
||||
const minPoints = options?.minPoints ?? MIN_LIVE_POINTS;
|
||||
/** Live telemetry only — no sample or blended filler in the dashboard. */
|
||||
export function resolveChartSeries(live: ChartPoint[]): { data: ChartPoint[]; mode: ChartDisplayMode } {
|
||||
const validation = validateChartSeries(live);
|
||||
const liveOk = validation.ok && live.length >= minPoints && hasMeaningfulLive(live, kind);
|
||||
|
||||
if (liveOk) {
|
||||
return { data: live.slice(-60), mode: 'live' };
|
||||
if (!validation.ok || live.length === 0) {
|
||||
return { data: [], mode: 'empty' };
|
||||
}
|
||||
|
||||
const sample = generateSampleSeries(kind, 48);
|
||||
if (live.length === 0) {
|
||||
if (options?.tailValue != null && Number.isFinite(options.tailValue)) {
|
||||
const last = sample[sample.length - 1];
|
||||
sample[sample.length - 1] = { ...last, value: options.tailValue };
|
||||
}
|
||||
return { data: sample, mode: 'sample' };
|
||||
}
|
||||
|
||||
const merged = [...sample.slice(0, Math.max(0, 48 - live.length)), ...live.slice(-24)];
|
||||
validateChartSeries(merged);
|
||||
return { data: merged, mode: 'blend' };
|
||||
return { data: live.slice(-60), mode: 'live' };
|
||||
}
|
||||
|
||||
export function chartSeriesDelta(data: ChartPoint[]): number | null {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { runForgeCompatibilityChecks } from './forgeCompatibility';
|
||||
import { runForgePreflight } from './forgeValidation';
|
||||
import { FORGE_BUILD_DEFAULTS } from './forgeDefaults';
|
||||
import type { BuildRequest } from '../types';
|
||||
|
||||
@@ -177,35 +178,37 @@ describe('runForgeCompatibilityChecks', () => {
|
||||
expect(hasCheck(baseForm({ process_name: 'bad name!' }), false, 'process_name', 'warn')).toBe(true);
|
||||
});
|
||||
|
||||
it('errors when worker name is empty', () => {
|
||||
expect(hasCheck(baseForm({ worker_name: '' }), false, 'worker_name_empty', 'error')).toBe(true);
|
||||
it('errors when worker name is empty (preflight)', () => {
|
||||
const checks = runForgePreflight(baseForm({ worker_name: '' }), false);
|
||||
expect(checks.find((c) => c.id === 'worker')?.level).toBe('error');
|
||||
});
|
||||
|
||||
it('errors when server URL uses localhost', () => {
|
||||
expect(
|
||||
hasCheck(baseForm({ server_url: 'http://localhost:8989' }), false, 'server_url_localhost', 'error')
|
||||
).toBe(true);
|
||||
it('errors when server URL uses localhost (preflight)', () => {
|
||||
const checks = runForgePreflight(baseForm({ server_url: 'http://localhost:8989' }), false);
|
||||
expect(checks.find((c) => c.id === 'server')?.level).toBe('error');
|
||||
});
|
||||
|
||||
it('errors when server URL uses 127.0.0.1', () => {
|
||||
expect(
|
||||
hasCheck(baseForm({ server_url: 'http://127.0.0.1:8989' }), false, 'server_url_localhost', 'error')
|
||||
).toBe(true);
|
||||
it('errors when server URL uses 127.0.0.1 (preflight)', () => {
|
||||
const checks = runForgePreflight(baseForm({ server_url: 'http://127.0.0.1:8989' }), false);
|
||||
expect(checks.find((c) => c.id === 'server')?.level).toBe('error');
|
||||
});
|
||||
|
||||
it('warns when wallet does not match Monero format', () => {
|
||||
expect(hasCheck(baseForm({ wallet: 'not-a-wallet' }), false, 'wallet_invalid', 'warn')).toBe(true);
|
||||
it('warns when wallet does not match Monero format (preflight)', () => {
|
||||
const checks = runForgePreflight(baseForm({ wallet: 'not-a-wallet' }), false);
|
||||
expect(checks.find((c) => c.id === 'wallet')?.level).toBe('warn');
|
||||
});
|
||||
|
||||
it('accepts minimum-length wallet (90 chars)', () => {
|
||||
it('accepts minimum-length wallet (90 chars) in preflight', () => {
|
||||
const wallet = '4' + 'A'.repeat(89);
|
||||
expect(wallet.length).toBe(90);
|
||||
expect(hasCheck(baseForm({ wallet }), false, 'wallet_invalid')).toBe(false);
|
||||
const checks = runForgePreflight(baseForm({ wallet }), false);
|
||||
expect(checks.find((c) => c.id === 'wallet')?.level).toBe('ok');
|
||||
});
|
||||
|
||||
it('accepts subaddress starting with 8', () => {
|
||||
it('accepts subaddress starting with 8 in preflight', () => {
|
||||
const subaddress = '8' + 'B'.repeat(94);
|
||||
expect(hasCheck(baseForm({ wallet: subaddress }), false, 'wallet_invalid')).toBe(false);
|
||||
const checks = runForgePreflight(baseForm({ wallet: subaddress }), false);
|
||||
expect(checks.find((c) => c.id === 'wallet')?.level).toBe('ok');
|
||||
});
|
||||
|
||||
it('emits forge_ready when core config is coherent', () => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { BuildRequest } from '../types';
|
||||
import type { PreflightCheck } from './forgeValidation';
|
||||
|
||||
function looksLikeXMRWallet(addr: string): boolean {
|
||||
const a = addr.trim();
|
||||
// Standard Monero addresses start with 4, subaddresses with 8
|
||||
return a.length >= 90 && a.length <= 106 && /^[48][0-9A-Za-z]+$/.test(a);
|
||||
}
|
||||
import { looksLikeXMRWallet } from './forgeValidation';
|
||||
|
||||
/** Extra incompatibility checks beyond basic validation. */
|
||||
export function runForgeCompatibilityChecks(form: BuildRequest, fusionPrepSelected: boolean): PreflightCheck[] {
|
||||
@@ -170,30 +165,6 @@ export function runForgeCompatibilityChecks(form: BuildRequest, fusionPrepSelect
|
||||
const workerName = form.worker_name || '';
|
||||
const serverUrl = form.server_url || '';
|
||||
|
||||
if (!workerName.trim()) {
|
||||
checks.push({
|
||||
id: 'worker_name_empty',
|
||||
level: 'error',
|
||||
message: 'Worker Name is required. This identifies the machine in your fleet.',
|
||||
});
|
||||
}
|
||||
|
||||
if (serverUrl.includes('localhost') || serverUrl.includes('127.0.0.1')) {
|
||||
checks.push({
|
||||
id: 'server_url_localhost',
|
||||
level: 'error',
|
||||
message: 'Control server URL uses localhost or 127.0.0.1 — deployed workers will try to connect to themselves instead of the server.',
|
||||
});
|
||||
}
|
||||
|
||||
if (wallet.trim() && !looksLikeXMRWallet(wallet)) {
|
||||
checks.push({
|
||||
id: 'wallet_invalid',
|
||||
level: 'warn',
|
||||
message: 'Wallet address does not match standard Monero format (starting with 4 or 8, length 90-106). Double check it.',
|
||||
});
|
||||
}
|
||||
|
||||
if (wallet.trim() && looksLikeXMRWallet(wallet) && poolHost.trim() && workerName.trim() && serverUrl.trim() && !serverUrl.includes('localhost') && !serverUrl.includes('127.0.0.1')) {
|
||||
checks.push({
|
||||
id: 'forge_ready',
|
||||
|
||||
@@ -37,8 +37,8 @@ const UNIVERSAL_INSTALL_BASES: InstallBaseOption[] = [
|
||||
];
|
||||
|
||||
export function deriveDeliverableType(form: BuildRequest): ForgeDeliverable {
|
||||
if (form.fusion_enabled) return 'fusion';
|
||||
if (form.spread_kit) return 'spread_kit';
|
||||
if (form.fusion_enabled) return 'fusion';
|
||||
return 'single';
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export function runForgePreflight(form: BuildRequest, fusionPrepSelected: boolea
|
||||
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).' });
|
||||
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.' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user