/** * Fleet API client — typed wrapper for the fleet coordinator endpoints. * Follows the same pattern as tracker-client.ts. * Degrades gracefully: 404s return null, network errors return defaults. */ import { createApiClient } from '@bytelyst/api-client'; // ── Types ─────────────────────────────────────────────────────────────────── export interface FleetJob { id: string; productId: string; stage: string; idempotencyKey: string; bodyMd: string; priority: string; priorityOrder: number; capabilities: string[]; kind: string; parentId?: string; attempts: number; leaseEpoch: number; createdAt: string; updatedAt: string; } export interface FleetFactory { id: string; productId: string; factoryId: string; capabilities: string[]; health: 'ok' | 'degraded' | 'down'; load: number; seatLimit: number; lastHeartbeatAt: string; } export interface FleetRun { id: string; jobId: string; attempt: number; factoryId?: string; engine: string; startedAt: string; endedAt?: string; result?: string; insights: Record; } export interface FleetEvent { id: string; jobId: string; seq: number; type: string; at: string; actor?: string; data: Record; } export interface FleetArtifact { id: string; jobId: string; kind: string; contentType: string; sizeBytes: number; createdAt: string; } export interface FleetBudget { id: string; productId: string; ceilingUsd: number; window: string; spentUsd: number; status: 'active' | 'paused'; updatedAt: string; } export interface DagNode { id: string; idempotencyKey: string; stage: string; priority: string; kind: string; parentId?: string; children: DagNode[]; } // ── Client ────────────────────────────────────────────────────────────────── const fleetApi = createApiClient({ baseUrl: '/api/fleet', getToken: () => (typeof window !== 'undefined' ? localStorage.getItem('tracker_token') : null), }); function apiFetch(path: string, options?: RequestInit): Promise { const extra: Record = {}; if (typeof window !== 'undefined') { const pid = localStorage.getItem('tracker_selected_product'); if (pid) extra['x-product-id'] = pid; } return fleetApi.fetch(path, { ...options, headers: { ...extra, ...(options?.headers as Record) }, }); } /** Graceful fetch — returns null on 404 instead of throwing. */ async function apiFetchOptional(path: string, options?: RequestInit): Promise { try { return await apiFetch(path, options); } catch (err: unknown) { if (err instanceof Error && err.message.includes('404')) return null; throw err; } } // ── Jobs ──────────────────────────────────────────────────────────────────── export interface ListJobsParams { stage?: string; productId?: string; limit?: number; offset?: number; } export async function listJobs(params?: ListJobsParams): Promise<{ jobs: FleetJob[] }> { const qs = params ? `?${new URLSearchParams(params as Record).toString()}` : ''; return apiFetch(`/jobs${qs}`); } export async function getJob(id: string): Promise { return apiFetchOptional(`/jobs/${id}`); } export async function patchJob( id: string, body: { leaseEpoch: number; stage: string } ): Promise { return apiFetch(`/jobs/${id}`, { method: 'PATCH', body: JSON.stringify(body) }); } export async function getJobRuns(jobId: string): Promise<{ runs: FleetRun[] }> { return apiFetch(`/jobs/${jobId}/runs`); } export async function getJobEvents(jobId: string): Promise<{ events: FleetEvent[] }> { return apiFetch(`/jobs/${jobId}/events`); } export async function getJobArtifacts(jobId: string): Promise<{ artifacts: FleetArtifact[] }> { return apiFetch(`/jobs/${jobId}/artifacts`); } export async function getJobDag(jobId: string): Promise<{ dag: DagNode } | null> { return apiFetchOptional(`/jobs/${jobId}/dag`); } // ── Factories ─────────────────────────────────────────────────────────────── export async function listFactories(): Promise<{ factories: FleetFactory[] }> { try { return await apiFetch('/factories'); } catch { return { factories: [] }; } } // ── Budgets ───────────────────────────────────────────────────────────────── export async function getBudget(productId: string): Promise { return apiFetchOptional(`/budgets/${productId}`); } export async function upsertBudget( productId: string, ceilingUsd: number, window: string ): Promise { return apiFetch(`/budgets/${productId}`, { method: 'PUT', body: JSON.stringify({ ceilingUsd, window }), }); } export async function pauseBudget(productId: string): Promise { return apiFetch(`/budgets/${productId}/pause`, { method: 'POST' }); } export async function resumeBudget(productId: string): Promise { return apiFetch(`/budgets/${productId}/resume`, { method: 'POST' }); }