feat(dev): serve Vite and FastAPI through the existing UI endpoint
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
# Existing s2s-client FastAPI server. Used only by Vite's server-side proxy.
|
# FastAPI target when running npm run dev independently. start_ui.sh sets this
|
||||||
S2S_BACKEND_URL=http://127.0.0.1:8420
|
# to its own backend using S2S_BACKEND_PORT (default 8421).
|
||||||
# Comma-separated hostnames, only needed when accessing Vite through a remote host.
|
S2S_BACKEND_URL=http://127.0.0.1:8421
|
||||||
|
# Comma-separated hostnames for remote access, without scheme or port.
|
||||||
|
# With start_ui.sh this can also be set in the repository root .env.
|
||||||
S2S_DEV_ALLOWED_HOSTS=
|
S2S_DEV_ALLOWED_HOSTS=
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createBackendProxy } from "../../config/backend-proxy";
|
|||||||
|
|
||||||
describe("development backend proxy", () => {
|
describe("development backend proxy", () => {
|
||||||
it("keeps the public host and original paths for the FastAPI runtime URL", () => {
|
it("keeps the public host and original paths for the FastAPI runtime URL", () => {
|
||||||
const proxy = createBackendProxy("http://127.0.0.1:8420");
|
const proxy = createBackendProxy("http://127.0.0.1:8421");
|
||||||
expect(proxy["/api"]).toEqual({ target: "http://127.0.0.1:8420", changeOrigin: false });
|
expect(proxy["/api"]).toEqual({ target: "http://127.0.0.1:8421", changeOrigin: false });
|
||||||
expect(proxy["/v1/realtime"]).toEqual({ target: "http://127.0.0.1:8420", changeOrigin: false, ws: true });
|
expect(proxy["/v1/realtime"]).toEqual({ target: "http://127.0.0.1:8421", changeOrigin: false, ws: true });
|
||||||
expect(proxy["/openapi.json"]).toEqual(proxy["/api"]);
|
expect(proxy["/openapi.json"]).toEqual(proxy["/api"]);
|
||||||
expect(proxy["/oauth"]).toEqual(proxy["/api"]);
|
expect(proxy["/oauth"]).toEqual(proxy["/api"]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ export default defineConfig(({ mode }) => {
|
|||||||
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
|
resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } },
|
||||||
server: {
|
server: {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 5173,
|
port: 8420,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
ws: { path: "/__vite_hmr" },
|
||||||
allowedHosts: (env.S2S_DEV_ALLOWED_HOSTS ?? "")
|
allowedHosts: (env.S2S_DEV_ALLOWED_HOSTS ?? "")
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((host) => host.trim())
|
.map((host) => host.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
proxy: createBackendProxy(env.S2S_BACKEND_URL ?? "http://127.0.0.1:8420"),
|
proxy: createBackendProxy(env.S2S_BACKEND_URL ?? "http://127.0.0.1:8421"),
|
||||||
},
|
},
|
||||||
build: { target: "es2022" },
|
build: { target: "es2022" },
|
||||||
};
|
};
|
||||||
|
|||||||
+124
-7
@@ -4,16 +4,45 @@ set -euo pipefail
|
|||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
VENV_DIR="$ROOT_DIR/.venv"
|
VENV_DIR="$ROOT_DIR/.venv"
|
||||||
REQ_FILE="$ROOT_DIR/s2s-client/requirements.txt"
|
REQ_FILE="$ROOT_DIR/s2s-client/requirements.txt"
|
||||||
UI_PORT="${UI_PORT:-8420}"
|
FRONTEND_DIR="$ROOT_DIR/frontend-s2s"
|
||||||
if [ -f "$ROOT_DIR/.env" ]; then
|
if [ -f "$ROOT_DIR/.env" ]; then
|
||||||
set -a
|
set -a
|
||||||
source "$ROOT_DIR/.env"
|
source "$ROOT_DIR/.env"
|
||||||
set +a
|
set +a
|
||||||
fi
|
fi
|
||||||
|
UI_PORT="${UI_PORT:-8420}"
|
||||||
|
UI_HOST="${UI_HOST:-0.0.0.0}"
|
||||||
|
S2S_BACKEND_PORT="${S2S_BACKEND_PORT:-8421}"
|
||||||
S2S_PROXY_TARGET="${S2S_PROXY_TARGET:-ws://ai-server:12345/v1/realtime}"
|
S2S_PROXY_TARGET="${S2S_PROXY_TARGET:-ws://ai-server:12345/v1/realtime}"
|
||||||
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
|
||||||
echo "Error: python3 was not found."
|
echo "Error: start_ui.sh requires Bash 4.4 or newer."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for required_command in python3 node npm setsid curl; do
|
||||||
|
if ! command -v "$required_command" >/dev/null 2>&1; then
|
||||||
|
echo "Error: $required_command was not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit((major === 22 && minor >= 18) || major >= 24 ? 0 : 1)'; then
|
||||||
|
echo "Error: frontend-s2s requires Node.js 22.18+ on the 22.x line, or Node.js 24+."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for port_name in UI_PORT S2S_BACKEND_PORT; do
|
||||||
|
port_value="${!port_name}"
|
||||||
|
if [[ ! "$port_value" =~ ^[0-9]{1,5}$ ]] || (( 10#$port_value < 1 || 10#$port_value > 65535 )); then
|
||||||
|
echo "Error: $port_name must be a port between 1 and 65535."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Normalize leading zeroes before passing ports to Node and Uvicorn.
|
||||||
|
printf -v "$port_name" '%d' "$((10#$port_value))"
|
||||||
|
done
|
||||||
|
if [ "$UI_PORT" = "$S2S_BACKEND_PORT" ]; then
|
||||||
|
echo "Error: UI_PORT and S2S_BACKEND_PORT must be different."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -29,17 +58,105 @@ if [ ! -f "$REQ_FILE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! python -c "import fastapi, uvicorn, httpx, huggingface_hub, dotenv" >/dev/null 2>&1; then
|
if ! python -c "import fastapi, uvicorn, httpx, huggingface_hub, dotenv, websockets" >/dev/null 2>&1; then
|
||||||
echo "Installing dependencies from s2s-client/requirements.txt..."
|
echo "Installing dependencies from s2s-client/requirements.txt..."
|
||||||
python -m pip install -r "$REQ_FILE"
|
python -m pip install -r "$REQ_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Keep restarts offline when the installed frontend manifests have not changed.
|
||||||
|
if [ ! -f "$FRONTEND_DIR/node_modules/vite/bin/vite.js" ] || \
|
||||||
|
! cmp -s "$FRONTEND_DIR/package-lock.json" "$FRONTEND_DIR/node_modules/.s2s-package-lock.json" || \
|
||||||
|
! cmp -s "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/node_modules/.s2s-package.json"; then
|
||||||
|
echo "Installing frontend dependencies from frontend-s2s/package-lock.json..."
|
||||||
|
npm --prefix "$FRONTEND_DIR" ci --include=dev
|
||||||
|
cp "$FRONTEND_DIR/package-lock.json" "$FRONTEND_DIR/node_modules/.s2s-package-lock.json"
|
||||||
|
cp "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/node_modules/.s2s-package.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The combined launcher always proxies to the backend it starts. An alternate
|
||||||
|
# S2S_BACKEND_URL is only used when running Vite independently.
|
||||||
|
export S2S_BACKEND_URL="http://127.0.0.1:$S2S_BACKEND_PORT"
|
||||||
export S2S_PROXY_TARGET
|
export S2S_PROXY_TARGET
|
||||||
|
|
||||||
echo "💻 Starting the S2S client UI (port $UI_PORT)..."
|
child_pids=()
|
||||||
|
cleanup() {
|
||||||
|
local exit_status=$?
|
||||||
|
trap - EXIT
|
||||||
|
trap '' INT TERM HUP
|
||||||
|
for child_pid in "${child_pids[@]}"; do
|
||||||
|
kill -TERM -- "-$child_pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
# Stop whole process groups, including Uvicorn's reload worker. Bound the
|
||||||
|
# graceful shutdown so a stuck connection cannot leave ports occupied.
|
||||||
|
for ((attempt = 0; attempt < 20; attempt++)); do
|
||||||
|
local alive=false
|
||||||
|
for child_pid in "${child_pids[@]}"; do
|
||||||
|
if kill -0 -- "-$child_pid" 2>/dev/null; then
|
||||||
|
alive=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$alive" = false ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
for child_pid in "${child_pids[@]}"; do
|
||||||
|
kill -KILL -- "-$child_pid" 2>/dev/null || true
|
||||||
|
wait "$child_pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
exit "$exit_status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
trap 'exit 129' HUP
|
||||||
|
|
||||||
|
echo "💻 Starting the React UI at http://$UI_HOST:$UI_PORT (Vite hot reload)..."
|
||||||
|
echo "🔧 FastAPI backend: $S2S_BACKEND_URL (Python reload enabled)"
|
||||||
echo "🔌 Realtime proxy target: $S2S_PROXY_TARGET"
|
echo "🔌 Realtime proxy target: $S2S_PROXY_TARGET"
|
||||||
echo " Press Ctrl+C in this terminal to stop it."
|
echo " Press Ctrl+C in this terminal to stop both servers."
|
||||||
echo "----------------------------------------------------"
|
echo "----------------------------------------------------"
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
PYTHONPATH=s2s-client python -m uvicorn server:app --host 0.0.0.0 --port "$UI_PORT" --reload
|
setsid "$VENV_DIR/bin/python" -m uvicorn server:app \
|
||||||
|
--app-dir "$ROOT_DIR/s2s-client" \
|
||||||
|
--host 127.0.0.1 --port "$S2S_BACKEND_PORT" \
|
||||||
|
--reload --reload-dir "$ROOT_DIR/s2s-client" &
|
||||||
|
backend_pid=$!
|
||||||
|
child_pids+=("$backend_pid")
|
||||||
|
|
||||||
|
backend_ready=false
|
||||||
|
startup_deadline=$((SECONDS + 30))
|
||||||
|
while (( SECONDS < startup_deadline )); do
|
||||||
|
if ! kill -0 "$backend_pid" 2>/dev/null; then
|
||||||
|
echo "Error: FastAPI exited before it was ready."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if curl --noproxy '*' --silent --fail --max-time 1 \
|
||||||
|
--output /dev/null "$S2S_BACKEND_URL/api/config"; then
|
||||||
|
backend_ready=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
if [ "$backend_ready" = false ]; then
|
||||||
|
echo "Error: FastAPI did not become ready within 30 seconds."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
exec setsid node node_modules/vite/bin/vite.js --host "$UI_HOST" --port "$UI_PORT" --strictPort
|
||||||
|
) &
|
||||||
|
frontend_pid=$!
|
||||||
|
child_pids+=("$frontend_pid")
|
||||||
|
|
||||||
|
# PM2 supervises this script. If either server exits, stop its sibling and fail
|
||||||
|
# the launcher so PM2 can restart the complete pair instead of a partial UI.
|
||||||
|
exit_status=0
|
||||||
|
wait -n "$backend_pid" "$frontend_pid" || exit_status=$?
|
||||||
|
echo "Error: a UI server exited; stopping both servers."
|
||||||
|
if [ "$exit_status" -eq 0 ]; then
|
||||||
|
exit_status=1
|
||||||
|
fi
|
||||||
|
exit "$exit_status"
|
||||||
|
|||||||
Reference in New Issue
Block a user