Persist build extra_files for Build Manager history, print dashboard login on every start, add libp2p for Mesh P2P forge, defer WebSocket until login, and split devrun.bat from LAUNCH.bat with USB deck auto-detection.
172 lines
6.5 KiB
TypeScript
172 lines
6.5 KiB
TypeScript
import type { Agent, Share, HashrateSample, BuildRecord, ServerConfig, BuildRequest, BuildResponse, ServerInfo, BlueprintInfo, FleetAlert, PoolStatus, AIActivityEntry, EarningsEstimate, FusionEstimate, XmrPrice } 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<T>(url: string, options?: RequestInit): Promise<T> {
|
|
const { headers: extraHeaders, ...rest } = options ?? {};
|
|
const res = await fetch(`${API_BASE}${url}`, {
|
|
...rest,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...authHeaders(),
|
|
...(extraHeaders as Record<string, string> | undefined),
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.text();
|
|
throw new Error(`API error ${res.status}: ${err}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
// Agents
|
|
listAgents: () => fetchJSON<Agent[]>('/agents'),
|
|
getAgent: (id: string) => fetchJSON<Agent>(`/agents/${id}`),
|
|
getDashboardStats: () => fetchJSON<{ total_agents: number; online_agents: number; total_hashrate: number; total_shares: number }>('/dashboard/stats'),
|
|
getAgentStats: (id: string, limit?: number) =>
|
|
fetchJSON<HashrateSample[]>(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`),
|
|
|
|
// Shares
|
|
getRecentShares: (limit?: number) =>
|
|
fetchJSON<Share[]>(`/shares${limit ? `?limit=${limit}` : ''}`),
|
|
|
|
// Builds
|
|
listBuilds: () => fetchJSON<BuildRecord[]>('/builds'),
|
|
|
|
// Config
|
|
getConfig: () => fetchJSON<ServerConfig>('/config'),
|
|
updateConfig: (config: Partial<ServerConfig>) =>
|
|
fetchJSON<ServerConfig>('/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<BuildResponse>;
|
|
});
|
|
}
|
|
return fetchJSON<BuildResponse>('/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<FusionEstimate>;
|
|
});
|
|
},
|
|
|
|
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<BlueprintInfo[]>('/blueprints'),
|
|
getBlueprint: (name: string) => fetchJSON<any>(`/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<ServerInfo>('/server/info'),
|
|
rotateFleetSecret: () =>
|
|
fetchJSON<{ ok: boolean; hint?: string }>('/server/rotate-secret', { method: 'POST' }),
|
|
|
|
// Fleet ops
|
|
getAlerts: () => fetchJSON<FleetAlert[]>('/alerts'),
|
|
getPoolStatus: () => fetchJSON<PoolStatus[]>('/pools/status'),
|
|
getAIActivity: () => fetchJSON<AIActivityEntry[]>('/ai/activity'),
|
|
getEarningsEstimate: (hashrate: number) =>
|
|
fetchJSON<EarningsEstimate>(`/earnings/estimate?hashrate=${encodeURIComponent(hashrate)}`),
|
|
sendAgentCommand: (
|
|
id: string,
|
|
action: string,
|
|
payload?: Record<string, unknown>
|
|
) =>
|
|
fetchJSON<{ success: boolean; error?: string }>(`/agents/${id}/command`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ action, ...payload }),
|
|
}),
|
|
getAgentLog: (id: string, refresh = false) =>
|
|
fetchJSON<{ agent_id: string; content: string }>(`/agents/${id}/log${refresh ? '?refresh=1' : ''}`),
|
|
|
|
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 }),
|
|
}),
|
|
|
|
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<XmrPrice>('/market/xmr'),
|
|
|
|
// Cancel an in-progress forge build by its cancel token.
|
|
cancelBuild: (cancelToken: string) =>
|
|
fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
};
|