docs: enterprise refactor plan with ralph specs
This commit is contained in:
74
frontend/src/hooks/useApi.ts
Normal file
74
frontend/src/hooks/useApi.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* useApi — fetch helper with error handling.
|
||||
*/
|
||||
|
||||
const BASE = '/api';
|
||||
|
||||
export async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: res.statusText }));
|
||||
throw new Error((body as { error?: string }).error ?? res.statusText);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
getSessions: () => apiFetch<import('../types').Session[]>('/sessions'),
|
||||
getSession: (id: string) => apiFetch<import('../types').Session>(`/sessions/${id}`),
|
||||
createSession: (body: { url: string; seed: number; config?: Partial<import('../types').ExplorationConfig> }) =>
|
||||
apiFetch<{ sessionId: string; status: string; startedAt: string }>('/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
stopSession: (id: string) =>
|
||||
apiFetch<{ stopped: boolean }>(`/sessions/${id}`, { method: 'DELETE' }),
|
||||
getSessionPerformance: (id: string) =>
|
||||
apiFetch<import('../types').PerformanceMetrics[]>(`/sessions/${id}/performance`),
|
||||
|
||||
getAnomalies: (params?: { sessionId?: string; severity?: string; type?: string }) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.sessionId) qs.set('sessionId', params.sessionId);
|
||||
if (params?.severity) qs.set('severity', params.severity);
|
||||
if (params?.type) qs.set('type', params.type);
|
||||
const q = qs.toString() ? `?${qs.toString()}` : '';
|
||||
return apiFetch<import('../types').AnomalySummary[]>(`/anomalies${q}`);
|
||||
},
|
||||
getAnomaly: (id: string) => apiFetch<import('../types').Anomaly>(`/anomalies/${id}`),
|
||||
replayAnomaly: (id: string) =>
|
||||
apiFetch<{ replayId: string; status: string }>(`/anomalies/${id}/replay`, { method: 'POST' }),
|
||||
enrichAnomaly: (id: string) =>
|
||||
apiFetch<{ status: string; anomalyId: string }>(`/anomalies/${id}/enrich`, { method: 'POST' }),
|
||||
|
||||
getStats: () => apiFetch<import('../types').Stats>('/stats'),
|
||||
getConfig: () => apiFetch<import('../types').ServerConfig>('/config'),
|
||||
patchConfig: (body: Partial<import('../types').ServerConfig>) =>
|
||||
apiFetch<import('../types').ServerConfig>('/config', { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
|
||||
// Schedules
|
||||
getSchedules: () => apiFetch<import('../types').Schedule[]>('/schedules'),
|
||||
createSchedule: (body: { name: string; url: string; cronExpression: string; config?: object; enabled?: boolean }) =>
|
||||
apiFetch<import('../types').Schedule>('/schedules', { method: 'POST', body: JSON.stringify(body) }),
|
||||
patchSchedule: (id: string, body: Partial<{ name: string; url: string; cronExpression: string; enabled: boolean }>) =>
|
||||
apiFetch<import('../types').Schedule>(`/schedules/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
deleteSchedule: (id: string) =>
|
||||
apiFetch<void>(`/schedules/${id}`, { method: 'DELETE' }),
|
||||
|
||||
// Visual regression
|
||||
getVisualComparisons: (params?: { sessionId?: string; status?: string }) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.sessionId) qs.set('sessionId', params.sessionId);
|
||||
if (params?.status) qs.set('status', params.status);
|
||||
const q = qs.toString() ? `?${qs.toString()}` : '';
|
||||
return apiFetch<import('../types').VisualComparison[]>(`/visual/comparisons${q}`);
|
||||
},
|
||||
approveBaseline: (comparisonId: string) =>
|
||||
apiFetch<{ baselineId: string; status: string }>(`/visual/baselines/${comparisonId}/approve`, { method: 'POST' }),
|
||||
rejectBaseline: (comparisonId: string) =>
|
||||
apiFetch<{ status: string }>(`/visual/baselines/${comparisonId}/reject`, { method: 'POST' }),
|
||||
approveAllBaselines: (sessionId?: string) =>
|
||||
apiFetch<{ approved: number }>('/visual/baselines/approve-all', { method: 'POST', body: JSON.stringify({ sessionId }) }),
|
||||
};
|
||||
40
frontend/src/hooks/useSocket.ts
Normal file
40
frontend/src/hooks/useSocket.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* useSocket — reusable socket.io-client connection.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
export type SocketHandler = (event: string, data: unknown) => void;
|
||||
|
||||
export function useSocket(onEvent: SocketHandler) {
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const handlerRef = useRef<SocketHandler>(onEvent);
|
||||
handlerRef.current = onEvent;
|
||||
|
||||
useEffect(() => {
|
||||
const socket = io({ path: '/socket.io' });
|
||||
socketRef.current = socket;
|
||||
|
||||
const events = [
|
||||
'session:started',
|
||||
'state:discovered',
|
||||
'action:executed',
|
||||
'anomaly:detected',
|
||||
'session:completed',
|
||||
'session:error',
|
||||
];
|
||||
|
||||
events.forEach((evt) => {
|
||||
socket.on(evt, (data: unknown) => handlerRef.current(evt, data));
|
||||
});
|
||||
|
||||
return () => { socket.disconnect(); };
|
||||
}, []);
|
||||
|
||||
const emit = useCallback((event: string, data: unknown) => {
|
||||
socketRef.current?.emit(event, data);
|
||||
}, []);
|
||||
|
||||
return { emit };
|
||||
}
|
||||
Reference in New Issue
Block a user