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:
AetherForge
2026-06-04 20:41:44 -07:00
parent 6bfce5d5ab
commit 8466c7aa9b
101 changed files with 3369 additions and 1054 deletions

View File

@@ -113,6 +113,14 @@ export default function AgentsPage() {
[agents]
);
useEffect(() => {
const liveIds = new Set(agents.map((a) => a.id));
setSelectedIds((prev) => {
const pruned = new Set([...prev].filter((id) => liveIds.has(id)));
return pruned.size === prev.size ? prev : pruned;
});
}, [agents]);
useEffect(() => {
if (!commandResults?.length || !screenshotWatchId.current) return;
const watch = screenshotWatchId.current;
@@ -516,7 +524,13 @@ export default function AgentsPage() {
</div>
<div className="detail-item">
<span className="detail-label">Wallet</span>
<span className="detail-value mono">{selectedAgent.wallet?.substring(0, 20)}...</span>
<span className="detail-value mono">
{selectedAgent.wallet
? selectedAgent.wallet.length > 24
? `${selectedAgent.wallet.slice(0, 20)}`
: selectedAgent.wallet
: '—'}
</span>
</div>
<div className="detail-item">
<span className="detail-label">IP Address</span>
@@ -597,9 +611,7 @@ export default function AgentsPage() {
time: new Date(s.timestamp).toLocaleTimeString(),
value: s.hashrate,
}));
const chart = resolveChartSeries(live, 'hashrate', {
tailValue: selectedAgent.hashrate_15m,
});
const chart = resolveChartSeries(live);
return (
<HashrateChart
title=""

View File

@@ -71,23 +71,27 @@ function CopyButton({ text, label }: { text: string; label: string }) {
);
}
function DeleteButton({ buildId, onDeleted }: { buildId: string; onDeleted: () => void }) {
function DeleteButton({ buildId, onDeleted, onError }: { buildId: string; onDeleted: () => void; onError: (msg: string) => void }) {
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleClick = () => {
const handleClick = async () => {
if (!confirming) {
setConfirming(true);
timerRef.current = setTimeout(() => setConfirming(false), 3000);
} else {
if (timerRef.current) clearTimeout(timerRef.current);
setBusy(true);
api.deleteBuild(buildId).finally(() => {
setBusy(false);
setConfirming(false);
onDeleted();
});
return;
}
if (timerRef.current) clearTimeout(timerRef.current);
setBusy(true);
try {
await api.deleteBuild(buildId);
onDeleted();
} catch (e) {
onError(e instanceof Error ? e.message : 'Failed to delete build');
} finally {
setBusy(false);
setConfirming(false);
}
};
@@ -96,7 +100,7 @@ function DeleteButton({ buildId, onDeleted }: { buildId: string; onDeleted: () =
type="button"
className={`bm-del-btn${confirming ? ' bm-del-btn-confirm' : ''}`}
disabled={busy}
onClick={handleClick}
onClick={() => void handleClick()}
title="Delete this build from server"
>
{busy ? '…' : confirming ? 'Confirm delete' : 'Delete'}
@@ -104,7 +108,17 @@ function DeleteButton({ buildId, onDeleted }: { buildId: string; onDeleted: () =
);
}
function PinButton({ buildId, pinned, onPinned }: { buildId: string; pinned: boolean; onPinned: () => void }) {
function PinButton({
buildId,
pinned,
onPinned,
onError,
}: {
buildId: string;
pinned: boolean;
onPinned: () => void;
onError: (msg: string) => void;
}) {
const [busy, setBusy] = useState(false);
const handleClick = async () => {
@@ -118,7 +132,7 @@ function PinButton({ buildId, pinned, onPinned }: { buildId: string; pinned: boo
}
onPinned();
} catch (e) {
console.error(e);
onError(e instanceof Error ? e.message : pinned ? 'Failed to unpin build' : 'Failed to pin build');
} finally {
setBusy(false);
}
@@ -145,12 +159,14 @@ function BuildCard({
onReforge,
onDeleted,
onPinned,
onActionError,
}: {
build: BuildRecord;
serverBase: string;
onReforge: (b: BuildRecord) => void;
onDeleted: () => void;
onPinned: () => void;
onActionError: (msg: string) => void;
}) {
const downloadUrl = `${window.location.origin}${api.buildDownloadUrl(build.id)}`;
const exeName = build.file_name || build.file_path?.replace(/^.*[/\\]/, '') || `worker-${build.worker_name}`;
@@ -249,7 +265,11 @@ function BuildCard({
{/* ── Dropper one-liners ── */}
<div className="bm-dropper">
<div className="bm-downloads-label font-tech">ONE-LINER DEPLOY (serves latest build)</div>
<div className="bm-downloads-label font-tech">
{build.pinned
? 'ONE-LINER DEPLOY (serves this pinned build)'
: 'ONE-LINER DEPLOY (serves latest build)'}
</div>
<div className="bm-dropper-row">
<span className="bm-dropper-os">Win</span>
<code className="bm-dropper-cmd">{ps1}</code>
@@ -274,7 +294,7 @@ function BuildCard({
<span className="bm-qr-label">Scan to download</span>
</div>
<div className="bm-action-btns">
<PinButton buildId={build.id} pinned={!!build.pinned} onPinned={onPinned} />
<PinButton buildId={build.id} pinned={!!build.pinned} onPinned={onPinned} onError={onActionError} />
<button
type="button"
className="btn btn-secondary bm-reforge-btn"
@@ -282,7 +302,7 @@ function BuildCard({
>
Re-forge
</button>
<DeleteButton buildId={build.id} onDeleted={onDeleted} />
<DeleteButton buildId={build.id} onDeleted={onDeleted} onError={onActionError} />
</div>
</div>
</NeonCard>
@@ -295,28 +315,31 @@ export default function BuildManagerPage() {
const [builds, setBuilds] = useState<BuildRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [serverBase, setServerBase] = useState('');
const [serverBase, setServerBase] = useState(() => window.location.origin.replace(/\/$/, ''));
const navigate = useNavigate();
const applyServerBase = useCallback((suggestedUrl?: string) => {
const pub = suggestedUrl?.trim().replace(/\/$/, '');
setServerBase(pub || window.location.origin.replace(/\/$/, ''));
}, []);
const loadBuilds = useCallback(async () => {
try {
const list = await api.listBuilds();
const [list, info] = await Promise.all([
api.listBuilds(),
api.getServerInfo().catch(() => null),
]);
setBuilds(list);
setError('');
if (info) applyServerBase(info.suggested_url);
else applyServerBase();
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load builds');
applyServerBase();
} finally {
setLoading(false);
}
// Load server base URL separately so a slow/hung server-info call
// never blocks the builds list from rendering.
api.getServerInfo()
.then((info) => {
const pub = info?.suggested_url?.trim().replace(/\/$/, '');
if (pub) setServerBase(pub);
})
.catch(() => {/* use window.location.origin fallback already set */});
}, []);
}, [applyServerBase]);
useEffect(() => { loadBuilds(); }, [loadBuilds]);
@@ -375,6 +398,7 @@ export default function BuildManagerPage() {
onReforge={handleReforge}
onDeleted={loadBuilds}
onPinned={loadBuilds}
onActionError={(msg) => setError(msg)}
/>
))}
</div>

View File

@@ -59,15 +59,17 @@ function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo, builds
return forgeDefaultsFromServerSmart(config, serverInfo, builds);
}
// Simulated stage timeline — (label, target% completed at this point, min ms from start)
// Simulated stage timeline — real compiles (garble/universal/fusion) often take 1030+ min.
// Cap below 95% until the server responds; finishForgeSuccess sets 100%.
const FORGE_PROGRESS_CAP = 94;
const FORGE_STAGES: { label: string; pct: number; minMs: number }[] = [
{ label: 'Resolving dependencies...', pct: 8, minMs: 0 },
{ label: 'Compiling agent source...', pct: 28, minMs: 800 },
{ label: 'Cross-compiling targets...', pct: 52, minMs: 2500 },
{ label: 'Applying obfuscation...', pct: 68, minMs: 5000 },
{ label: 'Packaging deliverable...', pct: 82, minMs: 8000 },
{ label: 'Signing & finalizing...', pct: 93, minMs: 11000 },
{ label: 'Almost done...', pct: 98, minMs: 15000 },
{ label: 'Resolving dependencies...', pct: 6, minMs: 0 },
{ label: 'Compiling agent source...', pct: 18, minMs: 20000 },
{ label: 'Cross-compiling targets...', pct: 36, minMs: 90000 },
{ label: 'Applying obfuscation...', pct: 52, minMs: 240000 },
{ label: 'Packaging deliverable...', pct: 68, minMs: 420000 },
{ label: 'Signing & finalizing...', pct: 82, minMs: 600000 },
{ label: 'Still forging (may take a while)...', pct: FORGE_PROGRESS_CAP, minMs: 900000 },
];
function ForgeProgressBar({ building, stage, progress }: { building: boolean; stage: string; progress: number }) {
@@ -153,6 +155,9 @@ export default function BuilderPage() {
const forgedThisSessionRef = useRef(false);
const [refreshingEndpoints, setRefreshingEndpoints] = useState(false);
const [simpleMode, setSimpleMode] = useState(loadSimpleMode);
const [pendingReforgeBuild, setPendingReforgeBuild] = useState<BuildRecord | null>(null);
const [highlightFusionPrep, setHighlightFusionPrep] = useState(false);
const fusionPrepRef = useRef<HTMLDivElement>(null);
// Drive simulated stage progress while a single build is running
useEffect(() => {
@@ -175,14 +180,14 @@ export default function BuilderPage() {
}
const s = FORGE_STAGES[next];
// Smoothly interpolate within this stage toward the next stage's target %
const nextPct = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].pct : 98;
const nextMs = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].minMs : 20000;
const nextPct = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].pct : FORGE_PROGRESS_CAP;
const nextMs = next + 1 < FORGE_STAGES.length ? FORGE_STAGES[next + 1].minMs : 1200000;
const stageElapsed = elapsed - s.minMs;
const stageDur = nextMs - s.minMs;
const frac = stageDur > 0 ? Math.min(1, stageElapsed / stageDur) : 0;
const pct = s.pct + (nextPct - s.pct) * frac;
if (next !== stageIdx) stageIdx = next;
setStage(s.label, Math.min(98, pct));
setStage(s.label, Math.min(FORGE_PROGRESS_CAP, pct));
forgeStageTimerRef.current = setTimeout(advance, 250);
};
advance();
@@ -238,8 +243,10 @@ export default function BuilderPage() {
setListenPort(config.port || 8989);
}
const candidates = info ? lanEndpointCandidates(info, config.port || info.port) : [];
const base = defaultsFromConfig(config, info ?? { port: config.port || 8989, host: '', local_ips: [], suggested_url: '', dashboard_url: '', websocket_url: '' }, builds as BuildRecord[]);
setForm(applySmartForgeDefaults(base, { builds: builds as BuildRecord[], endpointCandidates: candidates }));
const buildList = builds as BuildRecord[];
const base = defaultsFromConfig(config, info ?? { port: config.port || 8989, host: '', local_ips: [], suggested_url: '', dashboard_url: '', websocket_url: '' }, buildList);
setRecentBuilds(buildList);
setForm(applySmartForgeDefaults(base, { builds: buildList, endpointCandidates: candidates }));
})
.catch((err) => {
console.error(err);
@@ -257,17 +264,21 @@ export default function BuilderPage() {
}
};
// Handle ?reforge=<buildId> links from Build Manager page
// Handle ?reforge=<buildId> links from Build Manager — pre-fill only; user confirms before compile.
useEffect(() => {
const reforgeId = searchParams.get('reforge');
if (!reforgeId || recentBuilds.length === 0) return;
if (!reforgeId || !form || recentBuilds.length === 0) return;
const match = recentBuilds.find((b) => b.id === reforgeId);
if (match) {
reForgeFromBuild(match);
const merged = buildRequestFromRecord(match, form as unknown as Record<string, unknown>) as unknown as BuildRequest;
setForm(merged);
setPendingReforgeBuild(match);
setError('');
setLastBuild(null);
setSearchParams({}, { replace: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, recentBuilds]);
}, [searchParams, recentBuilds, form]);
const finishForgeSuccess = async (result: BuildResponse) => {
setStage('Build complete!', 100);
@@ -355,6 +366,12 @@ export default function BuilderPage() {
}
};
const focusFusionPrepPicker = () => {
setHighlightFusionPrep(true);
fusionPrepRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.setTimeout(() => setHighlightFusionPrep(false), 6000);
};
const reForgeFromBuild = async (build: BuildRecord) => {
if (!form) return;
const merged = buildRequestFromRecord(build, form as unknown as Record<string, unknown>) as unknown as BuildRequest;
@@ -366,8 +383,9 @@ export default function BuilderPage() {
// server after a build completes (M15). Prompt the user to re-upload first.
if (merged.fusion_enabled && !fusionPrepFile) {
setError(
'This build used a Fusion payload. Re-upload the payload file in the Fusion section above, then click "Re-forge" again.'
'This build used a Fusion payload. Re-upload the payload file in the Fusion section below, then confirm Re-forge again.'
);
focusFusionPrepPicker();
return;
}
@@ -651,7 +669,16 @@ export default function BuilderPage() {
await finishForgeSuccess(result);
} catch (err: any) {
if (err.message !== 'build cancelled') {
setError(err.message || 'Build failed');
let msg = err?.message || 'Build failed';
const aborted =
err?.name === 'AbortError' ||
/abort|timed out|timeout/i.test(msg);
if (aborted) {
msg =
'Forge request ended early (browser or proxy timeout). The server may still be compiling — open Build Manager or refresh this page in a minute.';
}
setError(msg);
void loadRecentBuilds();
}
} finally {
cancelTokenRef.current = '';
@@ -702,7 +729,7 @@ export default function BuilderPage() {
[form, fusionPrepFile]
);
const preflightChecks = useMemo(
() => (form ? runForgePreflight(form, !!fusionPrepFile) : []),
() => (form ? runForgePreflight(normalizeForgeForm(form), !!fusionPrepFile) : []),
[form, fusionPrepFile]
);
const canForge = form ? !preflightHasErrors(preflightChecks) : false;
@@ -811,6 +838,39 @@ export default function BuilderPage() {
return (
<div className="page fade-in command-deck">
<SetupBanner status={setupStatus} />
{pendingReforgeBuild && (
<div className="reforge-confirm-banner form-error" role="alert">
<span></span>
<div style={{ flex: 1 }}>
<strong>Re-forge {pendingReforgeBuild.worker_name}?</strong>
<p className="form-hint" style={{ margin: '0.35rem 0 0', color: 'inherit' }}>
Settings were loaded from Build Manager. Confirm to start compiling this cannot be undone mid-forge.
</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexShrink: 0 }}>
<button
type="button"
className="btn btn-primary btn-sm"
disabled={building}
onClick={() => {
const build = pendingReforgeBuild;
setPendingReforgeBuild(null);
void reForgeFromBuild(build);
}}
>
Confirm Re-forge
</button>
<button
type="button"
className="btn btn-outline btn-sm"
disabled={building}
onClick={() => setPendingReforgeBuild(null)}
>
Cancel
</button>
</div>
</div>
)}
{/* Hidden file input for importing blueprint .json files */}
<input
type="file"
@@ -1867,7 +1927,10 @@ export default function BuilderPage() {
{form.fusion_enabled && (
<>
{/* Single-file pick (used when Forge button is clicked) */}
<div className={`form-group ${fieldMeta.fusion_prep?.disabled ? 'field-disabled' : ''}`}>
<div
ref={fusionPrepRef}
className={`form-group fusion-prep-picker${highlightFusionPrep ? ' fusion-prep-highlight' : ''}${fieldMeta.fusion_prep?.disabled ? ' field-disabled' : ''}`}
>
<div className="label-row">
<label className="label">
Drop any file to fuse <HelpTip field="fusion_prep" />
@@ -1880,6 +1943,7 @@ export default function BuilderPage() {
accept="*"
onChange={(e) => {
applyFusionFileSelection(e.target.files?.[0] || null);
setHighlightFusionPrep(false);
e.target.value = '';
}}
/>

View File

@@ -123,12 +123,12 @@ describe('DashboardPage', () => {
expect(await screen.findByText('No miners on the wire')).toBeInTheDocument();
});
it('shows projection charts and wealth strip with no agents', async () => {
it('does not show projection or fake earnings with no agents', async () => {
renderDashboard();
expect(await screen.findByText(/Projection mode/i)).toBeInTheDocument();
expect(await screen.findByText('Fleet Hashrate Wave')).toBeInTheDocument();
expect(await screen.findByText('Accept Rate Pulse')).toBeInTheDocument();
expect(await screen.findByText('Target Fleet Earnings')).toBeInTheDocument();
expect(await screen.findByText('Command Deck')).toBeInTheDocument();
expect(screen.queryByText(/Projection mode/i)).not.toBeInTheDocument();
expect(screen.queryByText('Target Fleet Earnings')).not.toBeInTheDocument();
expect(screen.queryByText('Vault-01')).not.toBeInTheDocument();
});
it('renders stat labels and top agent card', async () => {

View File

@@ -12,7 +12,6 @@ import {
PoolStatusPanel,
AIActivityPanel,
EarningsEstimator,
WealthEarningsPreview,
FleetHealthCard,
ContributionBars,
UnderperformerList,
@@ -46,9 +45,6 @@ import {
} from '../help/fleetAnalytics';
import {
resolveChartSeries,
SAMPLE_ACTIVITY,
SAMPLE_CONTRIBUTION_BARS,
SAMPLE_FLEET_PREVIEW,
} from '../help/chartSampleData';
import './Pages.css';
@@ -133,6 +129,14 @@ export default function DashboardPage() {
}
}, [recentShares]);
useEffect(() => {
const liveIds = new Set(agents.map((a) => a.id));
setSelectedIds((prev) => {
const pruned = new Set([...prev].filter((id) => liveIds.has(id)));
return pruned.size === prev.size ? prev : pruned;
});
}, [agents]);
const totalHashrate = agents.reduce((sum, a) => sum + a.hashrate_15m, 0);
const onlineCount = agents.filter((a) => a.status === 'online').length;
const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0);
@@ -154,7 +158,7 @@ export default function DashboardPage() {
[gpuAgents]
);
const bestGPUAgent = useMemo(
() => gpuAgents.sort((a, b) => (b.gpu_hashrate_15m ?? 0) - (a.gpu_hashrate_15m ?? 0))[0] ?? null,
() => [...gpuAgents].sort((a, b) => (b.gpu_hashrate_15m ?? 0) - (a.gpu_hashrate_15m ?? 0))[0] ?? null,
[gpuAgents]
);
const gpuModels = useMemo(
@@ -166,10 +170,8 @@ export default function DashboardPage() {
const avgMem = agents.length > 0 ? agents.reduce((s, a) => s + a.memory_usage_pct, 0) / agents.length : 0;
const onlinePct = agents.length > 0 ? (onlineCount / agents.length) * 100 : 0;
const previewDeck = agents.length === 0 || (totalHashrate <= 0 && onlineCount === 0);
useEffect(() => {
if (previewDeck || totalHashrate <= 0) {
if (totalHashrate <= 0) {
setEstXmrDay(null);
return;
}
@@ -183,15 +185,7 @@ export default function DashboardPage() {
if (!controller.signal.aborted) setEstXmrDay(null);
});
return () => controller.abort();
}, [totalHashrate, previewDeck]);
const displayHashrate = previewDeck ? SAMPLE_FLEET_PREVIEW.hashrate : totalHashrate;
const displayAccept = previewDeck ? SAMPLE_FLEET_PREVIEW.acceptRate : acceptRate;
const displayCpu = previewDeck ? SAMPLE_FLEET_PREVIEW.avgCpu : avgCpu;
const displayMem = previewDeck ? SAMPLE_FLEET_PREVIEW.avgMem : avgMem;
const displayOnlinePct = previewDeck ? SAMPLE_FLEET_PREVIEW.onlinePct : onlinePct;
const displayOnline = previewDeck ? SAMPLE_FLEET_PREVIEW.onlineCount : onlineCount;
const displayAgentTotal = previewDeck ? SAMPLE_FLEET_PREVIEW.agentCount : agents.length;
}, [totalHashrate]);
useEffect(() => {
const now = new Date().toLocaleTimeString();
@@ -199,38 +193,21 @@ export default function DashboardPage() {
setAcceptHistory((prev) => [...prev.slice(-59), { time: now, value: acceptRate }]);
setCpuHistory((prev) => [...prev.slice(-59), { time: now, value: avgCpu }]);
setMemHistory((prev) => [...prev.slice(-59), { time: now, value: avgMem }]);
const gpuVal = totalGPUHashrate > 0 ? totalGPUHashrate : previewDeck ? 48_500_000 : 0;
if (gpuVal > 0 || previewDeck) {
setGpuHistory((prev) => [...prev.slice(-59), { time: now, value: gpuVal }]);
if (totalGPUHashrate > 0) {
setGpuHistory((prev) => [...prev.slice(-59), { time: now, value: totalGPUHashrate }]);
}
}, [totalHashrate, acceptRate, avgCpu, avgMem, totalGPUHashrate, previewDeck]);
}, [totalHashrate, acceptRate, avgCpu, avgMem, totalGPUHashrate]);
const hashChart = useMemo(
() => resolveChartSeries(hashHistory, 'hashrate', { tailValue: displayHashrate }),
[hashHistory, displayHashrate]
);
const acceptChart = useMemo(
() => resolveChartSeries(acceptHistory, 'accept', { tailValue: displayAccept }),
[acceptHistory, displayAccept]
);
const cpuChart = useMemo(
() => resolveChartSeries(cpuHistory, 'cpu', { tailValue: displayCpu }),
[cpuHistory, displayCpu]
);
const memChart = useMemo(
() => resolveChartSeries(memHistory, 'mem', { tailValue: displayMem }),
[memHistory, displayMem]
);
const gpuChart = useMemo(
() => resolveChartSeries(gpuHistory, 'gpu', { tailValue: totalGPUHashrate || 48_500_000 }),
[gpuHistory, totalGPUHashrate]
);
const hashChart = useMemo(() => resolveChartSeries(hashHistory), [hashHistory]);
const acceptChart = useMemo(() => resolveChartSeries(acceptHistory), [acceptHistory]);
const cpuChart = useMemo(() => resolveChartSeries(cpuHistory), [cpuHistory]);
const memChart = useMemo(() => resolveChartSeries(memHistory), [memHistory]);
const gpuChart = useMemo(() => resolveChartSeries(gpuHistory), [gpuHistory]);
const estUsdDay = useMemo(() => {
const price = xmrPrice ?? SAMPLE_FLEET_PREVIEW.xmrPrice;
const xmr = previewDeck ? SAMPLE_FLEET_PREVIEW.xmrPerDay : estXmrDay;
return xmr != null ? xmr * price : null;
}, [previewDeck, estXmrDay, xmrPrice]);
if (estXmrDay == null || xmrPrice == null) return null;
return estXmrDay * xmrPrice;
}, [estXmrDay, xmrPrice]);
const filteredAgents = useMemo(() => filterFleetAgents(agents, filters), [agents, filters]);
@@ -247,9 +224,8 @@ export default function DashboardPage() {
ok: s.accepted,
time: s.timestamp ? new Date(s.timestamp).toLocaleTimeString() : undefined,
}));
if (live.length > 0) return live;
return previewDeck ? SAMPLE_ACTIVITY : live;
}, [shares, previewDeck]);
return live;
}, [shares]);
const totalShareCount = agents.reduce((sum, a) => sum + a.shares_total, 0);
const alerts = fleetAlerts.length > 0 ? fleetAlerts : restAlerts;
@@ -427,12 +403,6 @@ export default function DashboardPage() {
<AuditLogStrip limit={6} />
</div>
{previewDeck && (
<p className="preview-deck-hint font-tech" role="status">
Projection mode charts validated with sample telemetry until your fleet connects
</p>
)}
<header className="deck-hero wealth-hero">
<div className="deck-hero-text">
<p className="deck-eyebrow font-tech">PERSONAL NETWORK · LIVE TELEMETRY</p>
@@ -470,7 +440,7 @@ export default function DashboardPage() {
<div className="deck-wealth-strip" aria-label="Fleet yield snapshot">
<div className="deck-wealth-pill">
<div className="dwp-label">Fleet Hash</div>
<div className="dwp-value mint">{formatHashrate(displayHashrate)}</div>
<div className="dwp-value mint">{formatHashrate(totalHashrate)}</div>
<div className="dwp-sub">15m rolling</div>
</div>
<div className="deck-wealth-pill">
@@ -478,19 +448,19 @@ export default function DashboardPage() {
<div className="dwp-value mint">
{estUsdDay != null ? `$${estUsdDay.toFixed(2)}` : '—'}
</div>
<div className="dwp-sub">{previewDeck ? 'projection' : 'from live hashrate'}</div>
<div className="dwp-sub">{totalHashrate > 0 ? 'from live hashrate' : 'no active hashing'}</div>
</div>
<div className="deck-wealth-pill">
<div className="dwp-label">Accept</div>
<div className="dwp-value">{displayAccept.toFixed(1)}%</div>
<div className="dwp-value">{acceptRate.toFixed(1)}%</div>
<div className="dwp-sub">share quality</div>
</div>
<div className="deck-wealth-pill">
<div className="dwp-label">Nodes Live</div>
<div className="dwp-value">
{displayOnline}/{displayAgentTotal}
{onlineCount}/{agents.length}
</div>
<div className="dwp-sub">{displayOnlinePct.toFixed(0)}% online</div>
<div className="dwp-sub">{onlinePct.toFixed(0)}% online</div>
</div>
</div>
@@ -514,8 +484,8 @@ export default function DashboardPage() {
<section className="gauge-row">
<NeonCard accent="cyan" className="gauge-card" hud>
<GaugeRing
value={displayHashrate}
max={Math.max(displayHashrate * 1.2, 1000)}
value={totalHashrate}
max={Math.max(totalHashrate * 1.2, 1000)}
label="Fleet Hash"
sublabel="15m avg"
color="var(--neon-cyan)"
@@ -524,21 +494,21 @@ export default function DashboardPage() {
</NeonCard>
<NeonCard accent="green" className="gauge-card" hud>
<GaugeRing
value={displayOnlinePct}
value={onlinePct}
label="Online"
sublabel={`${displayOnline}/${displayAgentTotal}`}
sublabel={`${onlineCount}/${agents.length}`}
color="var(--neon-green)"
size={110}
/>
</NeonCard>
<NeonCard accent="purple" className="gauge-card" hud>
<GaugeRing value={displayAccept} label="Accept" sublabel="share rate" color="var(--neon-purple)" size={110} />
<GaugeRing value={acceptRate} label="Accept" sublabel="share rate" color="var(--neon-purple)" size={110} />
</NeonCard>
<NeonCard accent="amber" className="gauge-card" hud>
<GaugeRing
value={displayCpu}
value={avgCpu}
label="CPU"
sublabel={`RAM ${displayMem.toFixed(0)}%`}
sublabel={`RAM ${avgMem.toFixed(0)}%`}
color="var(--neon-amber)"
size={110}
/>
@@ -548,26 +518,24 @@ export default function DashboardPage() {
<div className="grid-4 stats-grid steampunk-stats">
<NeonCard accent="cyan" className="stat-card-wrap wealth-stat">
<div className="stat-label font-tech">Total Hashrate</div>
<div className="stat-value hashrate neon-glow-cyan">{formatHashrate(displayHashrate)}</div>
<div className="stat-sub">{displayOnline} engines firing</div>
<div className="stat-value hashrate neon-glow-cyan">{formatHashrate(totalHashrate)}</div>
<div className="stat-sub">{onlineCount} engines firing</div>
</NeonCard>
{previewDeck ? (
<WealthEarningsPreview xmrPrice={xmrPrice} />
) : (
<EarningsEstimator hashrate={totalHashrate} xmrPrice={xmrPrice} />
)}
<EarningsEstimator hashrate={totalHashrate} xmrPrice={xmrPrice} />
<NeonCard accent="green" className="stat-card-wrap wealth-stat">
<div className="stat-label font-tech">Fleet Online</div>
<div className="stat-value accepted">
{displayOnline} <span className="stat-dim">/ {displayAgentTotal}</span>
{onlineCount} <span className="stat-dim">/ {agents.length}</span>
</div>
<div className="stat-sub">{displayAgentTotal - displayOnline} dormant</div>
<div className="stat-sub">{agents.length - onlineCount} dormant</div>
</NeonCard>
<NeonCard accent="purple" className="stat-card-wrap wealth-stat">
<div className="stat-label font-tech">Accept Rate</div>
<div className="stat-value neon-glow-purple">{displayAccept.toFixed(1)}%</div>
<div className="stat-value neon-glow-purple">{acceptRate.toFixed(1)}%</div>
<div className="stat-sub">
{previewDeck ? 'sample pool quality' : `${acceptedShares} valid · ${rejectedShares} rejected`}
{acceptedShares + rejectedShares > 0
? `${acceptedShares} valid · ${rejectedShares} rejected`
: 'no shares yet'}
</div>
</NeonCard>
<NeonCard accent="amber" className="stat-card-wrap">
@@ -819,9 +787,8 @@ export default function DashboardPage() {
{/* ── Analytics row — always visible ─────────────────────────────────── */}
<ContributionBars
bars={contribs.length > 0 ? contribs : previewDeck ? SAMPLE_CONTRIBUTION_BARS : []}
sample={previewDeck && contribs.length === 0}
xmrPerDay={previewDeck ? SAMPLE_FLEET_PREVIEW.xmrPerDay : undefined}
bars={contribs}
xmrPerDay={estXmrDay ?? undefined}
xmrPrice={xmrPrice}
/>
<UnderperformerList underperformers={underperformers} medianHashrate={medianHash} />
@@ -862,7 +829,7 @@ export default function DashboardPage() {
</div>
</Suspense>
{(hasGPUMining || previewDeck) && (
{hasGPUMining && (
<Suspense fallback={<ChartPlaceholder height={220} />}>
<NeonCard accent="gold" tilt3d className="chart-row" style={{ marginTop: '1rem' }}>
<HashrateChart
@@ -907,7 +874,7 @@ export default function DashboardPage() {
<span className="section-ornament"></span> Share Activity Pulse
<span className="section-line" />
</h2>
<ActivityPulse items={activityItems} sample={previewDeck && shares.length === 0} />
<ActivityPulse items={activityItems} />
</NeonCard>
<section className="section">

View File

@@ -501,6 +501,26 @@
margin-bottom: 1rem;
}
.reforge-confirm-banner {
color: var(--neon-cyan, #00e5ff);
background: rgba(0, 229, 255, 0.08);
border-color: rgba(0, 229, 255, 0.35);
}
.fusion-prep-picker.fusion-prep-highlight {
padding: 0.75rem;
border-radius: 8px;
outline: 2px solid var(--accent-red);
outline-offset: 2px;
background: rgba(239, 68, 68, 0.08);
animation: fusion-prep-pulse 1.2s ease-in-out 3;
}
@keyframes fusion-prep-pulse {
0%, 100% { outline-color: var(--accent-red); }
50% { outline-color: rgba(239, 68, 68, 0.35); }
}
.build-btn {
width: 100%;
justify-content: center;