Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions src/config/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
errorCode: number;
errorMessage: string | null;
payload: T;
}

/**
* Paged result wrapper for paginated endpoints
*/
Expand Down
62 changes: 29 additions & 33 deletions src/contexts/LivePVContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 39 additions & 15 deletions src/services/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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
Expand All @@ -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<T> = 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') {
Expand Down
6 changes: 3 additions & 3 deletions src/services/heartbeatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading