Lê Phi Nam commited on
Commit
f88dfcd
·
1 Parent(s): 68af9ee

Definitive fix: patch all 3 API URL sources for Docker same-origin serving

Browse files
Dockerfile CHANGED
@@ -11,38 +11,11 @@ COPY frontend/package.json ./frontend/
11
 
12
  RUN bun install --frozen-lockfile
13
 
14
- # Build static files (output lands in /app/frontend/dist)
15
  COPY frontend/ ./frontend/
16
 
17
- # Write a patcher script that replaces the API URL placeholder with
18
- # window.location.origin in the compiled JS output. Using bun to run
19
- # the script ensures it works on Alpine (no sed/find compatibility issues).
20
- RUN cat > /tmp/patch_api.mjs << 'PATCHEOF'
21
- import { readdirSync, statSync, readFileSync, writeFileSync } from "fs";
22
- import { join } from "path";
23
-
24
- function walk(dir) {
25
- for (const f of readdirSync(dir)) {
26
- const p = join(dir, f);
27
- if (statSync(p).isDirectory()) walk(p);
28
- else if (f.endsWith(".js")) {
29
- let c = readFileSync(p, "utf8");
30
- if (c.includes("__OMNIVOICE_RUNTIME_ORIGIN__")) {
31
- c = c.replaceAll('"__OMNIVOICE_RUNTIME_ORIGIN__"', "window.location.origin");
32
- c = c.replaceAll("'__OMNIVOICE_RUNTIME_ORIGIN__'", "window.location.origin");
33
- c = c.replaceAll("__OMNIVOICE_RUNTIME_ORIGIN__", "window.location.origin");
34
- writeFileSync(p, c);
35
- console.log("Patched:", p);
36
- }
37
- }
38
- }
39
- }
40
- walk("/app/frontend/dist");
41
- PATCHEOF
42
-
43
- # Build with placeholder, then patch compiled JS to use window.location.origin
44
- RUN VITE_API_URL="__OMNIVOICE_RUNTIME_ORIGIN__" bun run --cwd frontend build && \
45
- bun /tmp/patch_api.mjs
46
 
47
  # ==========================================
48
  # Runtime Stage: Python & PyTorch Backend
 
11
 
12
  RUN bun install --frozen-lockfile
13
 
14
+ # Copy frontend source (already patched for Docker same-origin serving)
15
  COPY frontend/ ./frontend/
16
 
17
+ # Build static files (output lands in /app/frontend/dist)
18
+ RUN bun run --cwd frontend build
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  # ==========================================
21
  # Runtime Stage: Python & PyTorch Backend
frontend/src/api/client.ts CHANGED
@@ -1,8 +1,10 @@
1
- // Backend base URL. Configurable via VITE_API_URL or VITE_API_PORT env vars.
2
- // In production Tauri builds, the webview talks to the sidecar on localhost.
3
- const viteEnv = import.meta.env ?? {};
4
- const _port = viteEnv.VITE_API_PORT || '3900';
5
- export const API = viteEnv.VITE_API_URL || `http://127.0.0.1:${_port}`;
 
 
6
 
7
  export class ApiError extends Error {
8
  status?: number;
 
1
+ // Docker/HF Spaces deployment: API client patched for same-origin serving.
2
+ // Backend and frontend share the same origin, so we use window.location.origin.
3
+ const _origin = typeof window !== 'undefined' && window.location
4
+ ? window.location.origin
5
+ : 'http://127.0.0.1:3900';
6
+
7
+ export const API = _origin;
8
 
9
  export class ApiError extends Error {
10
  status?: number;
frontend/src/components/CaptureWidget.jsx CHANGED
@@ -171,8 +171,7 @@ export default function CaptureWidget({ onDismiss }) {
171
  // Open WebSocket BEFORE starting recorder
172
  try {
173
  const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
174
- const wsHost = API_BASE.replace(/^https?:\/\//, '').replace(/\/$/, '')
175
- || `${window.location.hostname}:3900`;
176
  const wsUrl = `${wsProto}://${wsHost}/ws/transcribe`;
177
  const ws = new WebSocket(wsUrl);
178
  ws.binaryType = 'arraybuffer';
 
171
  // Open WebSocket BEFORE starting recorder
172
  try {
173
  const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
174
+ const wsHost = window.location.host;
 
175
  const wsUrl = `${wsProto}://${wsHost}/ws/transcribe`;
176
  const ws = new WebSocket(wsUrl);
177
  ws.binaryType = 'arraybuffer';
frontend/src/utils/apiBase.ts CHANGED
@@ -1,26 +1,8 @@
1
  /**
2
- * Centralised API base URL resolver.
3
  *
4
- * Single source of truth for "where is the OmniVoice backend reachable from
5
- * the currently-rendering frontend?". Three runtime contexts need different
6
- * answers:
7
- *
8
- * 1. Explicit override (Docker users / CI / power users):
9
- * VITE_OMNIVOICE_API="http://10.0.0.5:3900"
10
- * Always wins. Set in `.env.local` or the docker-compose env.
11
- *
12
- * 2. Tauri webview (the shipped desktop app):
13
- * Backend always listens on 127.0.0.1:3900 on the same machine.
14
- * Even when Tauri's webview origin is `tauri://localhost`, plain
15
- * `http://localhost:3900` reaches the backend.
16
- *
17
- * 3. Plain browser (Docker LAN, port-forward, dev server on a NAS):
18
- * The browser was served from some host — likely a LAN IP. We must
19
- * target THAT host's :3900, not the browser machine's localhost.
20
- * This closes issue #80 (Docker LAN frontend hits the wrong host).
21
- *
22
- * Plan: 01-03-PLAN.md (Phase 1 Wave 3)
23
- * Issue: #80
24
  */
25
 
26
  declare global {
@@ -30,11 +12,8 @@ declare global {
30
  }
31
  }
32
 
33
- /** Backend port — kept here as a single constant so we never grep-replace
34
- * hard-coded `3900` across the codebase again. */
35
  export const BACKEND_PORT = 3900;
36
 
37
- /** True when the current execution context is a Tauri webview. */
38
  export function isTauriContext(): boolean {
39
  return (
40
  typeof window !== "undefined" &&
@@ -42,47 +21,11 @@ export function isTauriContext(): boolean {
42
  );
43
  }
44
 
45
- /**
46
- * Resolve the backend API base URL for the current runtime context.
47
- *
48
- * Returns a URL with NO trailing slash so callers can safely concatenate
49
- * `/preview/upload` etc.
50
- */
51
- /** Test-only override for the env-resolved API base. vitest 4.x does not
52
- * propagate `vi.stubEnv` to dynamically imported modules' `import.meta.env`,
53
- * so we expose this small hook for tests. Production code never sets it. */
54
- let _testEnvOverride: string | undefined = undefined;
55
- export function _setEnvOverrideForTesting(value: string | undefined): void {
56
- _testEnvOverride = value;
57
- }
58
-
59
- function _readEnvOverride(): string | undefined {
60
- if (_testEnvOverride !== undefined) return _testEnvOverride;
61
- const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env;
62
- return env?.VITE_OMNIVOICE_API;
63
- }
64
-
65
  export function getApiBase(): string {
66
- // 1. Explicit override always wins.
67
- const override = _readEnvOverride();
68
- if (override) {
69
- return stripTrailingSlash(override);
70
- }
71
-
72
- // 2. Tauri webview → loopback.
73
- if (isTauriContext()) {
74
- return `http://localhost:${BACKEND_PORT}`;
75
- }
76
-
77
- // 3. Plain browser → follow the page's own origin/host.
78
  if (typeof window !== "undefined" && window.location) {
79
- const { protocol, hostname } = window.location;
80
- if (hostname) {
81
- return `${protocol}//${hostname}:${BACKEND_PORT}`;
82
- }
83
  }
84
-
85
- // 4. SSR / vitest jsdom without window — safe fallback.
86
  return `http://localhost:${BACKEND_PORT}`;
87
  }
88
 
@@ -90,9 +33,6 @@ function stripTrailingSlash(url: string): string {
90
  return url.endsWith("/") ? url.slice(0, -1) : url;
91
  }
92
 
93
- /** Module-level cached base URL — resolved once at import time. Most callers
94
- * want this; only call `getApiBase()` directly if you need to re-evaluate
95
- * after env or window changes (rare, mostly tests). */
96
  export const API_BASE: string = getApiBase();
97
 
98
  export default API_BASE;
 
1
  /**
2
+ * Centralised API base URL resolver — Docker/HF Spaces deployment.
3
  *
4
+ * In Docker, frontend and backend share the same origin (port 3900),
5
+ * so we simply use window.location.origin for everything.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  */
7
 
8
  declare global {
 
12
  }
13
  }
14
 
 
 
15
  export const BACKEND_PORT = 3900;
16
 
 
17
  export function isTauriContext(): boolean {
18
  return (
19
  typeof window !== "undefined" &&
 
21
  );
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  export function getApiBase(): string {
25
+ // Docker/HF Spaces: same-origin serving
 
 
 
 
 
 
 
 
 
 
 
26
  if (typeof window !== "undefined" && window.location) {
27
+ return window.location.origin;
 
 
 
28
  }
 
 
29
  return `http://localhost:${BACKEND_PORT}`;
30
  }
31
 
 
33
  return url.endsWith("/") ? url.slice(0, -1) : url;
34
  }
35
 
 
 
 
36
  export const API_BASE: string = getApiBase();
37
 
38
  export default API_BASE;