diff --git a/src/config/api.ts b/src/config/api.ts index d71a8a7..9c791b5 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -60,15 +60,6 @@ export function getWebSocketURL(path: string): string { return `${protocol}//${window.location.host}${path}`; } -/** - * Generic API response wrapper from squirrel-backend - */ -export interface ApiResultResponse { - errorCode: number; - errorMessage: string | null; - payload: T; -} - /** * Paged result wrapper for paginated endpoints */ diff --git a/src/contexts/LivePVContext.tsx b/src/contexts/LivePVContext.tsx index 99575c6..5792708 100644 --- a/src/contexts/LivePVContext.tsx +++ b/src/contexts/LivePVContext.tsx @@ -64,40 +64,36 @@ export function LivePVProvider({ throw new Error(`HTTP ${response.status}`); } - const data = await response.json(); - - if (data.errorCode === 0 && data.payload) { - const rawValues = data.payload as Record< - string, - { - value: unknown; - connected: boolean; - updated_at: number; - status?: string; - severity?: number; - units?: string; - } - >; - - // Transform backend format to EpicsData format - setLiveValues((prev) => { - const next = new Map(prev); - Object.entries(rawValues).forEach(([pvName, rawValue]) => { - // Map backend fields to EpicsData fields - const epicsData: EpicsData = { - data: rawValue.value as EpicsData['data'], - severity: rawValue.severity, - units: rawValue.units, - timestamp: rawValue.updated_at ? new Date(rawValue.updated_at * 1000) : undefined, - }; - next.set(pvName, epicsData); - }); - return next; + const rawValues = (await response.json()) as Record< + string, + { + value: unknown; + connected: boolean; + updated_at: number; + status?: string; + severity?: number; + units?: string; + } + >; + + // Transform backend format to EpicsData format + setLiveValues((prev) => { + const next = new Map(prev); + Object.entries(rawValues).forEach(([pvName, rawValue]) => { + // Map backend fields to EpicsData fields + const epicsData: EpicsData = { + data: rawValue.value as EpicsData['data'], + severity: rawValue.severity, + units: rawValue.units, + timestamp: rawValue.updated_at ? new Date(rawValue.updated_at * 1000) : undefined, + }; + next.set(pvName, epicsData); }); - setLastUpdate(new Date()); - setIsConnected(true); - setError(null); - } + return next; + }); + setLastUpdate(new Date()); + setIsConnected(true); + setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch live values'); setIsConnected(false); diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 9436ff8..bc2bd26 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -2,7 +2,41 @@ * Base API client for making HTTP requests to squirrel-backend */ -import { API_CONFIG, ApiKeyError, ApiResultResponse } from '../config/api'; +import { API_CONFIG, ApiKeyError } from '../config/api'; + +interface ValidationErrorItem { + loc?: (string | number)[]; + msg: string; + type?: string; +} + +async function extractErrorMessage(response: Response): Promise { + const fallback = `HTTP error! status: ${response.status}`; + let body: string; + try { + body = await response.text(); + } catch { + return fallback; + } + if (!body) return fallback; + + try { + const parsed = JSON.parse(body); + const { detail } = parsed; + if (typeof detail === 'string') return detail; + if (Array.isArray(detail)) { + return (detail as ValidationErrorItem[]) + .map((d) => `${d.loc?.join('.') ?? ''}: ${d.msg}`) + .join('; '); + } + } catch { + // not JSON — fall through + } + + // eslint-disable-next-line no-console + console.error('Server error response:', body); + return `${fallback}, details: ${body}`; +} class APIClient { // eslint-disable-next-line class-methods-use-this @@ -28,24 +62,14 @@ class APIClient { if (response.status === 401 || response.status === 403) { throw new ApiKeyError(response.status); } - // Try to get error details from response body - try { - const errorData = await response.text(); - // eslint-disable-next-line no-console - console.error('Server error response:', errorData); - throw new Error(`HTTP error! status: ${response.status}, details: ${errorData}`); - } catch { - throw new Error(`HTTP error! status: ${response.status}`); - } + throw new Error(await extractErrorMessage(response)); } - const data: ApiResultResponse = await response.json(); - - if (data.errorCode !== 0) { - throw new Error(data.errorMessage || 'API error'); + if (response.status === 204 || response.headers.get('content-length') === '0') { + return undefined as T; } - return data.payload; + return (await response.json()) as T; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { diff --git a/src/services/heartbeatService.ts b/src/services/heartbeatService.ts index bc54beb..0c88d69 100644 --- a/src/services/heartbeatService.ts +++ b/src/services/heartbeatService.ts @@ -65,9 +65,9 @@ class HeartbeatService { const data = await response.json(); // Backend returns: { alive: boolean, timestamp: number | null, age_seconds: number | null } - const alive = data.payload?.alive ?? false; - const age = data.payload?.age_seconds ?? null; - const timestamp = data.payload?.timestamp ?? null; + const alive = data.alive ?? false; + const age = data.age_seconds ?? null; + const timestamp = data.timestamp ?? null; this.lastKnownState = { alive, age, timestamp }; this.notifyCallbacks(alive, age);