Files
Khalid Abdi 403e0e38e9 frontend: runtime backend URL (LAN), voice dictation, version + update UI
Resolve the backend URL from the current host at runtime (lib/backend-url.ts)
so one build works on localhost and any clinic LAN IP without a rebuild; used
by the API client, auth client, and socket. Wire the AI-chat mic to the Web
Speech API with graceful fallback. Add a Settings "About & updates" panel
(current/latest version, update command, shareable network address) and an
optional dismissible update banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:34:30 +03:00

58 lines
1.4 KiB
TypeScript

// Thin fetch wrapper for the temetro backend's non-auth API (patients, …).
// Auth itself goes through the Better Auth client (lib/auth-client.ts).
import { resolveBackendUrl } from "@/lib/backend-url";
// Derived from the current host in the browser (see lib/backend-url.ts), so the
// app works over localhost and the clinic LAN without a per-deployment rebuild.
export const API_BASE_URL = resolveBackendUrl();
export class ApiError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
this.name = "ApiError";
}
}
export async function apiFetch<T>(
path: string,
init?: RequestInit,
): Promise<T> {
const res = await fetch(`${API_BASE_URL}${path}`, {
...init,
credentials: "include",
headers: {
"Content-Type": "application/json",
...init?.headers,
},
});
// Session missing/expired — bounce to login (browser only).
if (res.status === 401) {
if (typeof window !== "undefined") {
window.location.href = "/login";
}
throw new ApiError(401, "Not authenticated.");
}
if (res.status === 204) {
return undefined as T;
}
const body = (await res.json().catch(() => null)) as
| (T & { error?: string })
| null;
if (!res.ok) {
throw new ApiError(
res.status,
body?.error ?? `Request failed with status ${res.status}.`,
);
}
return body as T;
}