Add an optional single-process mode where the backend serves the built console bundle itself, so operators don't need a separate `npm run dev` for edge/dev setups. When RUNTIME_CONSOLE_STATIC_DIR points at the console dist directory, the app mounts a SpaStaticFiles handler at /ui/ (with 404 fallback to index.html for client-side routing) and redirects / to /ui/. The console build uses an empty VITE_API_BASE_URL for relative API paths (same-origin, no CORS), and Vite's base is set to /ui/ so assets resolve under the mount. /console/* JSON API is unchanged and is shared by both serve modes. api.ts now treats an explicitly-empty VITE_API_BASE_URL as "use relative paths" instead of falling back to the dev default, which previously forced absolute URLs even in same-origin builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
import type {
|
|
Device,
|
|
RegisterDevicePayload,
|
|
RuntimeConfig,
|
|
TaskRecord,
|
|
TimelineRecord,
|
|
} from "./types";
|
|
|
|
const configuredBaseUrl = import.meta.env.VITE_API_BASE_URL as string | undefined;
|
|
export const API_BASE_URL = (
|
|
configuredBaseUrl !== undefined ? configuredBaseUrl : "http://127.0.0.1:8000"
|
|
).replace(/\/$/, "");
|
|
|
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
...init,
|
|
headers: {
|
|
Accept: "application/json",
|
|
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
...init.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let message = `${response.status} ${response.statusText}`;
|
|
try {
|
|
const payload = (await response.json()) as { detail?: unknown };
|
|
if (typeof payload.detail === "string") {
|
|
message = payload.detail;
|
|
} else if (payload.detail) {
|
|
message = JSON.stringify(payload.detail);
|
|
}
|
|
} catch {
|
|
message = await response.text();
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
export function listDevices(): Promise<Device[]> {
|
|
return request<Device[]>("/console/devices");
|
|
}
|
|
|
|
export function registerDevice(payload: RegisterDevicePayload): Promise<Device> {
|
|
return request<Device>("/console/devices", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export function unregisterDevice(deviceId: string): Promise<void> {
|
|
return request<void>(`/console/devices/${encodeURIComponent(deviceId)}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
export function listTasks(filters: {
|
|
deviceId?: string;
|
|
status?: string;
|
|
}): Promise<TaskRecord[]> {
|
|
const params = new URLSearchParams();
|
|
if (filters.deviceId) {
|
|
params.set("device_id", filters.deviceId);
|
|
}
|
|
if (filters.status) {
|
|
params.set("status", filters.status);
|
|
}
|
|
const query = params.toString();
|
|
return request<TaskRecord[]>(`/console/tasks${query ? `?${query}` : ""}`);
|
|
}
|
|
|
|
export function getTask(taskId: string): Promise<TaskRecord> {
|
|
return request<TaskRecord>(`/console/tasks/${encodeURIComponent(taskId)}`);
|
|
}
|
|
|
|
export function getTimeline(taskId: string): Promise<TimelineRecord[]> {
|
|
return request<TimelineRecord[]>(
|
|
`/console/tasks/${encodeURIComponent(taskId)}/timeline`,
|
|
);
|
|
}
|
|
|
|
export function getConfig(): Promise<RuntimeConfig> {
|
|
return request<RuntimeConfig>("/console/config");
|
|
}
|
|
|
|
export function updateConfig(payload: RuntimeConfig): Promise<RuntimeConfig> {
|
|
return request<RuntimeConfig>("/console/config", {
|
|
method: "PUT",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|