Spaces:
Sleeping
Sleeping
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 +3 -30
- frontend/src/api/client.ts +7 -5
- frontend/src/components/CaptureWidget.jsx +1 -2
- frontend/src/utils/apiBase.ts +5 -65
Dockerfile
CHANGED
|
@@ -11,38 +11,11 @@ COPY frontend/package.json ./frontend/
|
|
| 11 |
|
| 12 |
RUN bun install --frozen-lockfile
|
| 13 |
|
| 14 |
-
#
|
| 15 |
COPY frontend/ ./frontend/
|
| 16 |
|
| 17 |
-
#
|
| 18 |
-
|
| 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 |
-
//
|
| 2 |
-
//
|
| 3 |
-
const
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
*
|
| 5 |
-
*
|
| 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 |
-
//
|
| 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 |
-
|
| 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;
|