Compare commits

...

9 Commits

Author SHA1 Message Date
Igor Loskutov
0d4c5c463c feat: standalone frontend uses production build instead of dev server
Override web service in docker-compose.standalone.yml to build from
www/Dockerfile (multi-stage: deps → build → standalone runner) instead
of running pnpm dev with bind-mounted source.
2026-02-12 11:27:45 -05:00
Igor Loskutov
f6a23cfddd fix: standalone GPU service connectivity with host network mode
Server runs with network_mode: host and can't resolve Docker service
names. Publish cpu port as 8100 on host, point server at localhost:8100.
Worker stays on bridge network using cpu:8000. Add dummy
TRANSCRIPT_MODAL_API_KEY since OpenAI SDK requires it even for local
endpoints.
2026-02-11 18:10:20 -05:00
Sergey Mankovsky
b1405af8c7 Remove turbopack 2026-02-11 23:15:32 +01:00
Sergey Mankovsky
71ad8a294f Fix webrtc connection 2026-02-11 23:11:46 +01:00
Sergey Mankovsky
bba272505f Enable server host mode 2026-02-11 23:11:31 +01:00
Igor Loskutov
67aea78243 fix: mock Celery broker in idle transcript validation test
test_validation_idle_transcript_with_recording_allowed called
validate_transcript_for_processing without mocking
task_is_scheduled_or_active, which attempts a real Celery
broker connection (AMQP port 5672). Other tests in the same
file already mock this — apply the same pattern here.
2026-02-11 16:26:24 -05:00
Igor Loskutov
2d81321733 fix: processing page auto-redirect after file upload completes
Three fixes for the processing page not redirecting when status becomes "ended":

- Add useWebSockets to processing page so it receives STATUS events
- Remove OAuth2PasswordBearer from auth_none — broke WebSocket endpoints (500)
- Reconnect stale Redis in ws_manager when Celery worker reuses dead event loop
2026-02-11 15:53:21 -05:00
Igor Loskutov
8c2b720564 fix: improve port conflict detection and ollama model check in standalone setup
- Filter OrbStack/Docker Desktop PIDs from port conflict check (false positives on Mac)
- Check all infra ports (5432, 6379, 3900, 3903) not just app ports
- Fix ollama model detection to match on name column only
- Document OrbStack and cross-project port conflicts in troubleshooting
2026-02-11 14:17:19 -05:00
Sergey Mankovsky
88e945ec00 Add hatchet env vars 2026-02-11 20:02:29 +01:00
11 changed files with 110 additions and 41 deletions

View File

@@ -0,0 +1,10 @@
# Standalone Compose: Remaining Production Work
## Server/worker/beat: remove host network mode + bind mounts
Currently `server` uses `network_mode: host` and all three services bind-mount `./server/:/app/`. For full standalone prod:
- Remove `network_mode: host` from server
- Remove bind-mount volumes from server, worker, beat (use built image only)
- Update `compose_cmd` in `setup-standalone.sh` to not rely on host network
- Change `SERVER_API_URL` from `http://host.docker.internal:1250` to `http://server:1250` (server reachable via Docker network once off host mode)

View File

@@ -63,21 +63,34 @@ services:
server: server:
environment: environment:
TRANSCRIPT_BACKEND: modal TRANSCRIPT_BACKEND: modal
TRANSCRIPT_URL: http://cpu:8000 TRANSCRIPT_URL: http://localhost:8100
TRANSCRIPT_MODAL_API_KEY: local
DIARIZATION_BACKEND: modal DIARIZATION_BACKEND: modal
DIARIZATION_URL: http://cpu:8000 DIARIZATION_URL: http://localhost:8100
worker: worker:
environment: environment:
TRANSCRIPT_BACKEND: modal TRANSCRIPT_BACKEND: modal
TRANSCRIPT_URL: http://cpu:8000 TRANSCRIPT_URL: http://cpu:8000
TRANSCRIPT_MODAL_API_KEY: local
DIARIZATION_BACKEND: modal DIARIZATION_BACKEND: modal
DIARIZATION_URL: http://cpu:8000 DIARIZATION_URL: http://cpu:8000
web:
image: reflector-frontend-standalone
build:
context: ./www
command: ["node", "server.js"]
volumes: !reset []
environment:
NODE_ENV: production
cpu: cpu:
build: build:
context: ./gpu/self_hosted context: ./gpu/self_hosted
dockerfile: Dockerfile.cpu dockerfile: Dockerfile.cpu
ports:
- "8100:8000"
volumes: volumes:
- gpu_cache:/root/.cache - gpu_cache:/root/.cache
restart: unless-stopped restart: unless-stopped

View File

@@ -2,8 +2,7 @@ services:
server: server:
build: build:
context: server context: server
ports: network_mode: host
- 1250:1250
volumes: volumes:
- ./server/:/app/ - ./server/:/app/
- /app/.venv - /app/.venv
@@ -11,8 +10,12 @@ services:
- ./server/.env - ./server/.env
environment: environment:
ENTRYPOINT: server ENTRYPOINT: server
extra_hosts: DATABASE_URL: postgresql+asyncpg://reflector:reflector@localhost:5432/reflector
- "host.docker.internal:host-gateway" REDIS_HOST: localhost
CELERY_BROKER_URL: redis://localhost:6379/1
CELERY_RESULT_BACKEND: redis://localhost:6379/1
HATCHET_CLIENT_SERVER_URL: http://localhost:8889
HATCHET_CLIENT_HOST_PORT: localhost:7078
worker: worker:
build: build:
@@ -24,6 +27,11 @@ services:
- ./server/.env - ./server/.env
environment: environment:
ENTRYPOINT: worker ENTRYPOINT: worker
HATCHET_CLIENT_SERVER_URL: http://hatchet:8888
HATCHET_CLIENT_HOST_PORT: hatchet:7077
depends_on:
redis:
condition: service_started
beat: beat:
build: build:
@@ -35,6 +43,9 @@ services:
- ./server/.env - ./server/.env
environment: environment:
ENTRYPOINT: beat ENTRYPOINT: beat
depends_on:
redis:
condition: service_started
hatchet-worker-cpu: hatchet-worker-cpu:
build: build:
@@ -46,6 +57,8 @@ services:
- ./server/.env - ./server/.env
environment: environment:
ENTRYPOINT: hatchet-worker-cpu ENTRYPOINT: hatchet-worker-cpu
HATCHET_CLIENT_SERVER_URL: http://hatchet:8888
HATCHET_CLIENT_HOST_PORT: hatchet:7077
depends_on: depends_on:
hatchet: hatchet:
condition: service_healthy condition: service_healthy
@@ -59,8 +72,8 @@ services:
- ./server/.env - ./server/.env
environment: environment:
ENTRYPOINT: hatchet-worker-llm ENTRYPOINT: hatchet-worker-llm
extra_hosts: HATCHET_CLIENT_SERVER_URL: http://hatchet:8888
- "host.docker.internal:host-gateway" HATCHET_CLIENT_HOST_PORT: hatchet:7077
depends_on: depends_on:
hatchet: hatchet:
condition: service_healthy condition: service_healthy
@@ -84,6 +97,11 @@ services:
- ./www/.env.local - ./www/.env.local
environment: environment:
- NODE_ENV=development - NODE_ENV=development
- SERVER_API_URL=http://host.docker.internal:1250
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- server
postgres: postgres:
image: postgres:17 image: postgres:17
@@ -99,13 +117,14 @@ services:
- ./server/docker/init-hatchet-db.sql:/docker-entrypoint-initdb.d/init-hatchet-db.sql:ro - ./server/docker/init-hatchet-db.sql:/docker-entrypoint-initdb.d/init-hatchet-db.sql:ro
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -d reflector -U reflector"] test: ["CMD-SHELL", "pg_isready -d reflector -U reflector"]
interval: 10s interval: 5s
timeout: 10s timeout: 5s
retries: 5 retries: 10
start_period: 10s start_period: 15s
hatchet: hatchet:
image: ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest image: ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest
restart: on-failure
ports: ports:
- "8889:8888" - "8889:8888"
- "7078:7077" - "7078:7077"
@@ -113,7 +132,7 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
environment: environment:
DATABASE_URL: "postgresql://reflector:reflector@postgres:5432/hatchet?sslmode=disable" DATABASE_URL: "postgresql://reflector:reflector@postgres:5432/hatchet?sslmode=disable&connect_timeout=30"
SERVER_AUTH_COOKIE_DOMAIN: localhost SERVER_AUTH_COOKIE_DOMAIN: localhost
SERVER_AUTH_COOKIE_INSECURE: "t" SERVER_AUTH_COOKIE_INSECURE: "t"
SERVER_GRPC_BIND_ADDRESS: "0.0.0.0" SERVER_GRPC_BIND_ADDRESS: "0.0.0.0"
@@ -135,7 +154,3 @@ services:
volumes: volumes:
next_cache: next_cache:
networks:
default:
attachable: true

View File

@@ -168,6 +168,8 @@ If the frontend or backend behaves unexpectedly (e.g., env vars seem ignored, ch
lsof -i :3000 # frontend lsof -i :3000 # frontend
lsof -i :1250 # backend lsof -i :1250 # backend
lsof -i :5432 # postgres lsof -i :5432 # postgres
lsof -i :3900 # Garage S3 API
lsof -i :6379 # Redis
# Kill stale processes on a port # Kill stale processes on a port
lsof -ti :3000 | xargs kill lsof -ti :3000 | xargs kill
@@ -175,9 +177,13 @@ lsof -ti :3000 | xargs kill
Common causes: Common causes:
- A stale `next dev` or `pnpm dev` process from another terminal/worktree - A stale `next dev` or `pnpm dev` process from another terminal/worktree
- Another Docker Compose project (different worktree) with containers on the same ports - Another Docker Compose project (different worktree) with containers on the same ports — the setup script only manages its own project; containers from other projects must be stopped manually (`docker ps` to find them, `docker stop` to kill them)
The setup script checks for port conflicts before starting services. The setup script checks ports 3000, 1250, 5432, 6379, 3900, 3903 for conflicts before starting services. It ignores OrbStack/Docker Desktop port forwarding processes (which always bind these ports but are not real conflicts).
### OrbStack false port-conflict warnings (Mac)
If you use OrbStack as your Docker runtime, `lsof` will show OrbStack binding ports like 3000, 1250, etc. even when no containers are running. This is OrbStack's port forwarding mechanism — not a real conflict. The setup script filters these out automatically.
### Re-enabling authentication ### Re-enabling authentication

View File

@@ -113,7 +113,7 @@ step_llm() {
echo "" echo ""
# Pull model if not already present # Pull model if not already present
if ollama list 2>/dev/null | grep -q "$MODEL"; then if ollama list 2>/dev/null | awk '{print $1}' | grep -qx "$MODEL"; then
ok "Model $MODEL already pulled" ok "Model $MODEL already pulled"
else else
info "Pulling model $MODEL (this may take a while)..." info "Pulling model $MODEL (this may take a while)..."
@@ -143,7 +143,7 @@ step_llm() {
echo "" echo ""
# Pull model inside container # Pull model inside container
if compose_cmd exec "$OLLAMA_SVC" ollama list 2>/dev/null | grep -q "$MODEL"; then if compose_cmd exec "$OLLAMA_SVC" ollama list 2>/dev/null | awk '{print $1}' | grep -qx "$MODEL"; then
ok "Model $MODEL already pulled" ok "Model $MODEL already pulled"
else else
info "Pulling model $MODEL inside container (this may take a while)..." info "Pulling model $MODEL inside container (this may take a while)..."
@@ -290,16 +290,23 @@ ENVEOF
step_services() { step_services() {
info "Step 5: Starting Docker services" info "Step 5: Starting Docker services"
# Check for port conflicts — stale processes silently shadow Docker port mappings # Check for port conflicts — stale processes silently shadow Docker port mappings.
# OrbStack/Docker Desktop bind ports for forwarding; ignore those PIDs.
local ports_ok=true local ports_ok=true
for port in 3000 1250; do for port in 3000 1250 5432 6379 3900 3903; do
local pid local pids
pid=$(lsof -ti :"$port" 2>/dev/null || true) pids=$(lsof -ti :"$port" 2>/dev/null || true)
if [[ -n "$pid" ]]; then for pid in $pids; do
warn "Port $port already in use by PID $pid" local pname
pname=$(ps -p "$pid" -o comm= 2>/dev/null || true)
# OrbStack and Docker Desktop own port forwarding — not real conflicts
if [[ "$pname" == *"OrbStack"* ]] || [[ "$pname" == *"com.docker"* ]] || [[ "$pname" == *"vpnkit"* ]]; then
continue
fi
warn "Port $port already in use by PID $pid ($pname)"
warn "Kill it with: lsof -ti :$port | xargs kill" warn "Kill it with: lsof -ti :$port | xargs kill"
ports_ok=false ports_ok=false
fi done
done done
if [[ "$ports_ok" == "false" ]]; then if [[ "$ports_ok" == "false" ]]; then
warn "Port conflicts detected — Docker containers may not be reachable" warn "Port conflicts detected — Docker containers may not be reachable"

View File

@@ -1,11 +1,5 @@
from typing import Annotated
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel from pydantic import BaseModel
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
class UserInfo(BaseModel): class UserInfo(BaseModel):
sub: str sub: str
@@ -15,13 +9,13 @@ class AccessTokenInfo(BaseModel):
pass pass
def authenticated(token: Annotated[str, Depends(oauth2_scheme)]): def authenticated():
return None return None
def current_user(token: Annotated[str, Depends(oauth2_scheme)]): def current_user():
return None return None
def current_user_optional(token: Annotated[str, Depends(oauth2_scheme)]): def current_user_optional():
return None return None

View File

@@ -48,7 +48,15 @@ class RedisPubSubManager:
if not self.redis_connection: if not self.redis_connection:
await self.connect() await self.connect()
message = json.dumps(message) message = json.dumps(message)
await self.redis_connection.publish(room_id, message) try:
await self.redis_connection.publish(room_id, message)
except RuntimeError:
# Celery workers run each task in a new event loop (asyncio.run),
# which closes the previous loop. Cached Redis connection is dead.
# Reconnect on the current loop and retry.
self.redis_connection = None
await self.connect()
await self.redis_connection.publish(room_id, message)
async def subscribe(self, room_id: str) -> redis.Redis: async def subscribe(self, room_id: str) -> redis.Redis:
await self.pubsub.subscribe(room_id) await self.pubsub.subscribe(room_id)

View File

@@ -291,7 +291,12 @@ async def test_validation_idle_transcript_with_recording_allowed():
recording_id="test-recording-id", recording_id="test-recording-id",
) )
result = await validate_transcript_for_processing(mock_transcript) with patch(
"reflector.services.transcript_process.task_is_scheduled_or_active"
) as mock_celery_check:
mock_celery_check.return_value = False
result = await validate_transcript_for_processing(mock_transcript)
assert isinstance(result, ValidationOk) assert isinstance(result, ValidationOk)
assert result.recording_id == "test-recording-id" assert result.recording_id == "test-recording-id"

View File

@@ -11,6 +11,7 @@ import {
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranscriptGet } from "../../../../lib/apiHooks"; import { useTranscriptGet } from "../../../../lib/apiHooks";
import { parseNonEmptyString } from "../../../../lib/utils"; import { parseNonEmptyString } from "../../../../lib/utils";
import { useWebSockets } from "../../useWebSockets";
type TranscriptProcessing = { type TranscriptProcessing = {
params: Promise<{ params: Promise<{
@@ -24,6 +25,7 @@ export default function TranscriptProcessing(details: TranscriptProcessing) {
const router = useRouter(); const router = useRouter();
const transcript = useTranscriptGet(transcriptId); const transcript = useTranscriptGet(transcriptId);
useWebSockets(transcriptId);
useEffect(() => { useEffect(() => {
const status = transcript.data?.status; const status = transcript.data?.status;

View File

@@ -23,7 +23,16 @@ const useWebRTC = (
let p: Peer; let p: Peer;
try { try {
p = new Peer({ initiator: true, stream: stream }); p = new Peer({
initiator: true,
stream: stream,
// Disable trickle ICE: single SDP exchange (offer + answer) with all candidates.
// Required for HTTP-based signaling; trickle needs WebSocket for candidate exchange.
trickle: false,
config: {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
},
});
} catch (error) { } catch (error) {
setError(error as Error, "Error creating WebRTC"); setError(error as Error, "Error creating WebRTC");
return; return;

View File

@@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"build": "next build", "build": "next build",
"build-production": "next build --experimental-build-mode compile", "build-production": "next build --experimental-build-mode compile",
"start": "next start", "start": "next start",