mirror of
https://github.com/temetro/temetro.git
synced 2026-08-31 04:47:57 +00:00
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>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
// Thin fetch wrapper for the temetro backend's non-auth API (patients, …).
|
||||
// Auth itself goes through the Better Auth client (lib/auth-client.ts).
|
||||
|
||||
export const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
||||
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(
|
||||
|
||||
@@ -5,12 +5,15 @@ import {
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
import { ac, roles } from "@/lib/access";
|
||||
import { resolveBackendUrl } from "@/lib/backend-url";
|
||||
|
||||
// The backend (Express + Better Auth) is a separate origin. The client appends
|
||||
// `/api/auth` to this base URL and always sends credentials so the session
|
||||
// cookie set by the backend is included on every request.
|
||||
// cookie set by the backend is included on every request. The base URL is
|
||||
// derived from the current host (see lib/backend-url.ts) so login works over
|
||||
// localhost and the clinic LAN alike.
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000",
|
||||
baseURL: resolveBackendUrl(),
|
||||
// usernameClient enables signIn.username(...) so admin-provisioned staff can
|
||||
// log in with a username; organizationClient powers clinics + RBAC.
|
||||
plugins: [usernameClient(), organizationClient({ ac, roles })],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Resolves the backend's base URL at runtime instead of baking it in at build.
|
||||
//
|
||||
// temetro ships as a prebuilt image and is reached from many hosts — the
|
||||
// server's own machine (localhost) and other departments over the LAN (the
|
||||
// host's IP, e.g. http://192.168.1.20:3000). A URL baked at build time can't
|
||||
// serve all of them, so in the browser we derive the backend URL from the page
|
||||
// the user actually loaded (same hostname, the backend's port). Set
|
||||
// NEXT_PUBLIC_API_URL to override for custom/reverse-proxied deployments.
|
||||
|
||||
const ENV_URL = process.env.NEXT_PUBLIC_API_URL?.trim();
|
||||
// Backend port; override only if you publish the API on a non-default port.
|
||||
const API_PORT = process.env.NEXT_PUBLIC_API_PORT?.trim() || "4000";
|
||||
|
||||
/** The backend origin to call from the current context. */
|
||||
export function resolveBackendUrl(): string {
|
||||
if (ENV_URL) return ENV_URL;
|
||||
if (typeof window !== "undefined") {
|
||||
return `${window.location.protocol}//${window.location.hostname}:${API_PORT}`;
|
||||
}
|
||||
// Server-side fallback (SSR/build); real browser calls use the branch above.
|
||||
return "http://localhost:4000";
|
||||
}
|
||||
@@ -1032,6 +1032,8 @@
|
||||
"attachFiles": "Attach files",
|
||||
"removeFile": "Remove {{name}}",
|
||||
"dictate": "Dictate",
|
||||
"dictateStop": "Stop dictation",
|
||||
"dictateUnsupported": "Voice dictation isn't supported in this browser",
|
||||
"send": "Send",
|
||||
"stop": "Stop",
|
||||
"model": "Model",
|
||||
@@ -1485,7 +1487,30 @@
|
||||
"signing": "Signing",
|
||||
"careTeam": "Care team",
|
||||
"integrations": "Integrations",
|
||||
"developers": "Developers"
|
||||
"developers": "Developers",
|
||||
"version": "About & updates"
|
||||
},
|
||||
"version": {
|
||||
"title": "Version",
|
||||
"description": "The temetro version running on this server, and whether a newer release is available.",
|
||||
"current": "Current version",
|
||||
"latest": "Latest release",
|
||||
"checking": "Checking for updates…",
|
||||
"upToDate": "Up to date",
|
||||
"updateAvailable": "Update available",
|
||||
"offline": "Couldn't reach the update server",
|
||||
"updateTitle": "How to update",
|
||||
"updateDescription": "temetro is self-hosted with Docker. To update, pull the new images and restart on the server machine.",
|
||||
"updateCommandLabel": "Update command",
|
||||
"viewRelease": "View release notes",
|
||||
"networkTitle": "Network access",
|
||||
"networkDescription": "Let other departments open temetro from their own computers on the hospital network using this address.",
|
||||
"networkUrlLabel": "Network address",
|
||||
"networkLocalHint": "You're viewing temetro locally. To share it, open the app on the server machine using its network IP address (e.g. from ipconfig / ifconfig), then share that URL.",
|
||||
"networkFirewallHint": "The server's firewall must allow the port for other devices to connect.",
|
||||
"banner": "A new version of temetro ({{version}}) is available.",
|
||||
"bannerUpdate": "See how to update",
|
||||
"bannerDismiss": "Later"
|
||||
},
|
||||
"integrations": {
|
||||
"loading": "Loading integrations…",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
|
||||
import { API_BASE_URL } from "@/lib/api-client";
|
||||
import { resolveBackendUrl } from "@/lib/backend-url";
|
||||
|
||||
// A single shared Socket.io connection to the backend, authenticated by the
|
||||
// Better Auth session cookie (withCredentials). Used by messaging and
|
||||
@@ -9,7 +9,8 @@ let socket: Socket | null = null;
|
||||
|
||||
export function getSocket(): Socket {
|
||||
if (!socket) {
|
||||
socket = io(API_BASE_URL, {
|
||||
// Resolved lazily in the browser so it targets the host actually in use.
|
||||
socket = io(resolveBackendUrl(), {
|
||||
withCredentials: true,
|
||||
transports: ["websocket", "polling"],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Client helpers for the backend's version + network endpoints (both public).
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
export type VersionInfo = {
|
||||
current: string;
|
||||
latest: string | null;
|
||||
updateAvailable: boolean;
|
||||
releaseUrl: string | null;
|
||||
};
|
||||
|
||||
export type NetworkInfo = {
|
||||
port: number;
|
||||
addresses: string[];
|
||||
urls: string[];
|
||||
};
|
||||
|
||||
export function getVersionInfo(): Promise<VersionInfo> {
|
||||
return apiFetch<VersionInfo>("/api/version");
|
||||
}
|
||||
|
||||
export function getNetworkInfo(): Promise<NetworkInfo> {
|
||||
return apiFetch<NetworkInfo>("/api/network");
|
||||
}
|
||||
Reference in New Issue
Block a user