learning_ai_common_plat/dashboards/tracker-web/src/lib/fleet-client.ts
Saravanakumar D b061cc47f3 feat(tracker-web): fleet control plane UI — overview, jobs, budget, detail pages (Phase 3 Slice 4)
- Fleet overview page with factory cards + recent jobs polling
- Job table with stage filter tabs
- Job detail page with events timeline, runs, artifacts, DAG subtree, SHIP action
- Budget page with usage bar, pause/resume controls
- API proxy route forwarding /api/fleet/* to platform-service
- Typed fleet-client.ts with graceful 404 degradation
- 16 unit tests for fleet-client (198 total tracker-web tests green)
- Added Fleet nav item to dashboard layout
- Full monorepo build + test green

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-30 09:49:24 -07:00

194 lines
5.6 KiB
TypeScript

/**
* 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<string, unknown>;
}
export interface FleetEvent {
id: string;
jobId: string;
seq: number;
type: string;
at: string;
actor?: string;
data: Record<string, unknown>;
}
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<T>(path: string, options?: RequestInit): Promise<T> {
const extra: Record<string, string> = {};
if (typeof window !== 'undefined') {
const pid = localStorage.getItem('tracker_selected_product');
if (pid) extra['x-product-id'] = pid;
}
return fleetApi.fetch<T>(path, {
...options,
headers: { ...extra, ...(options?.headers as Record<string, string>) },
});
}
/** Graceful fetch — returns null on 404 instead of throwing. */
async function apiFetchOptional<T>(path: string, options?: RequestInit): Promise<T | null> {
try {
return await apiFetch<T>(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<string, string>).toString()}` : '';
return apiFetch(`/jobs${qs}`);
}
export async function getJob(id: string): Promise<FleetJob | null> {
return apiFetchOptional(`/jobs/${id}`);
}
export async function patchJob(
id: string,
body: { leaseEpoch: number; stage: string }
): Promise<FleetJob> {
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<FleetBudget | null> {
return apiFetchOptional(`/budgets/${productId}`);
}
export async function upsertBudget(
productId: string,
ceilingUsd: number,
window: string
): Promise<FleetBudget> {
return apiFetch(`/budgets/${productId}`, {
method: 'PUT',
body: JSON.stringify({ ceilingUsd, window }),
});
}
export async function pauseBudget(productId: string): Promise<FleetBudget> {
return apiFetch(`/budgets/${productId}/pause`, { method: 'POST' });
}
export async function resumeBudget(productId: string): Promise<FleetBudget> {
return apiFetch(`/budgets/${productId}/resume`, { method: 'POST' });
}