fix: ssl error when using self signed certificate (#238)

fix: ssl error when using self signed certificate with remote deployment
This commit is contained in:
Abhishek 2026-04-14 18:58:27 +05:30 committed by GitHub
parent 7fab959e26
commit 50a59164e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 285 additions and 292 deletions

View file

@ -207,6 +207,28 @@ server {
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Backend API and WebSockets — bypass the UI, go straight to api:8000
location /api/v1/ {
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# Long-lived WebSockets (audio streaming, signaling)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Don't buffer streamed responses
proxy_buffering off;
client_max_body_size 100M;
}
location / {
proxy_pass http://ui:3010;
proxy_http_version 1.1;
@ -248,16 +270,12 @@ echo -e "${GREEN}✓ nginx.conf updated${NC}"
# Update .env file with domain name
echo -e "${BLUE}[6/8] Updating environment variables...${NC}"
if [[ -f ".env" ]]; then
# Update BACKEND_API_ENDPOINT to use domain
# Update BACKEND_API_ENDPOINT to use domain (public URL the backend advertises)
sed -i.bak "s|^BACKEND_API_ENDPOINT=.*|BACKEND_API_ENDPOINT=https://$DOMAIN_NAME|" .env
# Update BACKEND_URL if present, otherwise add it
if grep -q "^BACKEND_URL=" .env; then
sed -i.bak "s|^BACKEND_URL=.*|BACKEND_URL=https://$DOMAIN_NAME|" .env
else
echo "" >> .env
echo "# Backend URL for UI" >> .env
echo "BACKEND_URL=https://$DOMAIN_NAME" >> .env
fi
# Drop any stale BACKEND_URL override — the ui container should use the
# internal Docker URL (http://api:8000) from docker-compose defaults.
sed -i.bak "/^BACKEND_URL=/d" .env
sed -i.bak "/^# Backend URL for UI$/d" .env
# Update TURN_HOST to use domain
sed -i.bak "s|^TURN_HOST=.*|TURN_HOST=$DOMAIN_NAME|" .env
rm -f .env.bak

View file

@ -15,9 +15,11 @@ echo "║ Automated HTTPS deployment with TURN server ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
# Get the public IP address
echo -e "${YELLOW}Enter your server's public IP address:${NC}"
read -p "> " SERVER_IP
# Get the public IP address (skip prompt if SERVER_IP is already set)
if [[ -z "$SERVER_IP" ]]; then
echo -e "${YELLOW}Enter your server's public IP address:${NC}"
read -p "> " SERVER_IP
fi
if [[ -z "$SERVER_IP" ]]; then
echo -e "${RED}Error: IP address cannot be empty${NC}"
@ -30,29 +32,39 @@ if ! [[ "$SERVER_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
exit 1
fi
# Get the TURN secret
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
read -sp "> " TURN_SECRET
echo ""
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
if [[ -z "$TURN_SECRET" ]]; then
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
read -sp "> " TURN_SECRET
echo ""
fi
if [[ -z "$TURN_SECRET" ]]; then
TURN_SECRET=$(openssl rand -hex 32)
echo -e "${BLUE}Generated random TURN secret${NC}"
fi
# Telemetry opt-out (default: true)
ENABLE_TELEMETRY="${ENABLE_TELEMETRY:-true}"
echo ""
echo -e "${GREEN}Configuration:${NC}"
echo -e " Server IP: ${BLUE}$SERVER_IP${NC}"
echo -e " TURN Secret: ${BLUE}********${NC}"
echo ""
# Create project directory if it doesn't exist
mkdir -p dograh 2>/dev/null || true
cd dograh
# Create project directory and download compose file (skip when
# DOGRAH_SKIP_DOWNLOAD=1 — e.g. e2e tests that already have a cloned repo).
if [[ "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then
mkdir -p dograh 2>/dev/null || true
cd dograh
echo -e "${BLUE}[1/5] Downloading docker-compose.yaml...${NC}"
curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml
echo -e "${GREEN}✓ docker-compose.yaml downloaded${NC}"
echo -e "${BLUE}[1/5] Downloading docker-compose.yaml...${NC}"
curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml
echo -e "${GREEN}✓ docker-compose.yaml downloaded${NC}"
else
echo -e "${BLUE}[1/5] Using docker-compose.yaml in current directory${NC}"
fi
echo -e "${BLUE}[2/5] Creating nginx.conf...${NC}"
cat > nginx.conf << 'NGINX_EOF'
@ -75,6 +87,28 @@ server {
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# Backend API and WebSockets — bypass the UI, go straight to api:8000
location /api/v1/ {
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# Long-lived WebSockets (audio streaming, signaling)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Don't buffer streamed responses
proxy_buffering off;
client_max_body_size 100M;
}
location / {
proxy_pass http://ui:3010;
proxy_http_version 1.1;
@ -170,12 +204,9 @@ echo -e "${BLUE}[6/6] Creating environment file...${NC}"
OSS_JWT_SECRET=$(openssl rand -hex 32)
cat > .env << ENV_EOF
# Backend API endpoint (for remote deployment)
# Backend API endpoint (public URL the backend uses to build webhook/embed links)
BACKEND_API_ENDPOINT=https://$SERVER_IP
# Backend URL for UI
BACKEND_URL=https://$SERVER_IP
# TURN Server Configuration (time-limited credentials via TURN REST API)
TURN_HOST=$SERVER_IP
TURN_SECRET=$TURN_SECRET
@ -184,7 +215,7 @@ TURN_SECRET=$TURN_SECRET
OSS_JWT_SECRET=$OSS_JWT_SECRET
# Telemetry (set to false to disable)
ENABLE_TELEMETRY=true
ENABLE_TELEMETRY=$ENABLE_TELEMETRY
ENV_EOF
echo -e "${GREEN}✓ .env file created${NC}"

View file

@ -6,19 +6,10 @@ import type { HealthResponse } from "@/client/types.gen";
// Import version from package.json at build time
import packageJson from "../../../../../package.json";
// Internal/local URLs that are not reachable from the browser
const INTERNAL_HOST_RE = /^https?:\/\/(localhost|127\.0\.0\.1|api)(:\d+)?(\/|$)/;
function isInternalUrl(url: string | undefined | null): boolean {
return !url || INTERNAL_HOST_RE.test(url);
}
export async function GET() {
const uiVersion = packageJson.version || "dev";
// Fetch backend version and config from health endpoint
let apiVersion = "unknown";
let backendApiEndpoint: string | null = null;
let deploymentMode = "oss";
let authProvider = "local";
@ -27,28 +18,16 @@ export async function GET() {
if (response.data) {
const data = response.data as HealthResponse;
apiVersion = data.version;
// Pass through the backend's own endpoint for display purposes
backendApiEndpoint = data.backend_api_endpoint;
deploymentMode = data.deployment_mode;
authProvider = data.auth_provider;
}
} catch {
// Backend might not be reachable during build or in some deployments
apiVersion = "unavailable";
}
// For the API client base URL: prefer BACKEND_URL env, fall back to
// health endpoint value. Skip internal/Docker-only URLs (e.g. http://api:8000)
// that aren't reachable from the browser — the client will keep using
// window.location.origin via the Next.js proxy instead.
const clientCandidate = process.env.BACKEND_URL || backendApiEndpoint;
const clientApiBaseUrl = isInternalUrl(clientCandidate) ? 'http://localhost:8000' : clientCandidate;
return NextResponse.json({
ui: uiVersion,
api: apiVersion,
backendApiEndpoint,
clientApiBaseUrl,
deploymentMode,
authProvider,
});

View file

@ -1,140 +1,15 @@
'use client';
import Link from 'next/link';
import { Suspense } from 'react';
import { useEffect, useRef, useState } from 'react';
import { getIntegrationsApiV1IntegrationGet } from "@/client/sdk.gen";
import { getServerAccessToken,getServerAuthProvider } from '@/lib/auth/server';
import type { IntegrationResponse } from '@/client/types.gen';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import CreateIntegrationButton from "./CreateIntegrationButton";
export const dynamic = 'force-dynamic';
// Server component for integration list
async function IntegrationList() {
const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
const { redirect } = await import('next/navigation');
if (authProvider === 'stack') {
redirect('/');
} else {
// For OSS mode, this shouldn't happen as token is auto-generated
return (
<div className="text-red-500">
Authentication required. Please refresh the page.
</div>
);
}
}
try {
const response = await getIntegrationsApiV1IntegrationGet({
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const integrationData = response.data ? (Array.isArray(response.data) ? response.data : [response.data]) : [];
const integrations = [...integrationData].sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
if (integrations.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
No integrations found. Create your first integration to get started.
</div>
);
}
return (
<div className="space-y-6">
<div className="overflow-x-auto">
<table className="min-w-full bg-card border border-border">
<thead className="bg-muted">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Channel
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Action
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Created At
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-card divide-y divide-border">
{integrations.map((integration) => (
<tr key={integration.id} className="hover:bg-muted/50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{integration.provider}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'slack' && integration.provider_data ? (integration.provider_data.channel as string) || '-' : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.action}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{new Date(integration.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'google-mail' && (
<Link
href={`/integrations/${integration.id}/gmail`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Search
</Link>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
} catch (err) {
logger.error(`Error fetching integrations: ${err}`);
return (
<div className="text-red-500 text-center py-8">
Failed to load Integrations. Please Try Again Later.
</div>
);
}
}
async function PageContent() {
const integrationList = await IntegrationList();
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Your Integrations</h1>
<CreateIntegrationButton />
</div>
{integrationList}
</div>
</div>
);
}
function IntegrationsLoading() {
return (
<div className="container mx-auto px-4 py-8">
@ -187,9 +62,113 @@ function IntegrationsLoading() {
}
export default function IntegrationsPage() {
const { user, loading: authLoading } = useAuth();
const hasFetched = useRef(false);
const [integrations, setIntegrations] = useState<IntegrationResponse[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (authLoading || !user || hasFetched.current) return;
hasFetched.current = true;
const fetchIntegrations = async () => {
try {
const response = await getIntegrationsApiV1IntegrationGet({});
const integrationData = response.data ? (Array.isArray(response.data) ? response.data : [response.data]) : [];
const sorted = [...integrationData].sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
setIntegrations(sorted);
} catch (err) {
logger.error(`Error fetching integrations: ${err}`);
setError('Failed to load Integrations. Please Try Again Later.');
}
};
fetchIntegrations();
}, [authLoading, user]);
if (authLoading || (integrations === null && !error)) {
return <IntegrationsLoading />;
}
return (
<Suspense fallback={<IntegrationsLoading />}>
<PageContent />
</Suspense>
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Your Integrations</h1>
<CreateIntegrationButton />
</div>
{error ? (
<div className="text-red-500 text-center py-8">{error}</div>
) : !integrations || integrations.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No integrations found. Create your first integration to get started.
</div>
) : (
<div className="space-y-6">
<div className="overflow-x-auto">
<table className="min-w-full bg-card border border-border">
<thead className="bg-muted">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Channel
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Action
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Created At
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-card divide-y divide-border">
{integrations.map((integration) => (
<tr key={integration.id} className="hover:bg-muted/50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{integration.provider}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'slack' && integration.provider_data ? (integration.provider_data.channel as string) || '-' : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.action}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{new Date(integration.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{integration.provider === 'google-mail' && (
<Link
href={`/integrations/${integration.id}/gmail`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Search
</Link>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -1,99 +1,22 @@
'use client';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import { useParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet } from '@/client/sdk.gen';
import type { TestSessionResponse } from '@/client/types.gen';
import { ConversationsList } from '@/components/looptalk/ConversationsList';
import { LiveAudioPlayer } from '@/components/looptalk/LiveAudioPlayer';
import { TestSessionControls } from '@/components/looptalk/TestSessionControls';
import { TestSessionDetails } from '@/components/looptalk/TestSessionDetails';
import { Button } from '@/components/ui/button';
import { getServerAccessToken,getServerAuthProvider } from '@/lib/auth/server';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import LoopTalkLayout from "../LoopTalkLayout";
interface PageProps {
params: Promise<{
id: string;
}>;
}
async function PageContent({ params }: PageProps) {
const authProvider = await getServerAuthProvider();
const accessToken = await getServerAccessToken();
if (!accessToken) {
const { redirect } = await import('next/navigation');
if (authProvider === 'stack') {
redirect('/');
} else {
// For OSS mode, this shouldn't happen as token is auto-generated
return (
<div className="text-red-500">
Authentication required. Please refresh the page.
</div>
);
}
}
try {
const resolvedParams = await params;
const testSessionId = parseInt(resolvedParams.id);
const response = await getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet({
path: {
test_session_id: testSessionId
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const testSession = response.data;
if (!testSession) {
notFound();
}
// Transform the API response to match our UI types
const sessionForUI = {
id: testSession.id,
name: testSession.name,
description: '', // API doesn't return description
test_type: testSession.test_index !== null ? 'load_test' : 'single',
status: testSession.status,
actor_workflow_name: `Workflow ${testSession.actor_workflow_id}`, // We'll need to fetch actual names
adversary_workflow_name: `Workflow ${testSession.adversary_workflow_id}`,
created_at: testSession.created_at,
updated_at: testSession.created_at, // API doesn't have updated_at
test_metadata: testSession.config
};
return (
<div className="container mx-auto px-4 py-8">
<TestSessionDetails session={sessionForUI} />
<TestSessionControls session={sessionForUI} />
{/* Persistent Audio Player */}
<div className="mt-6">
<LiveAudioPlayer
testSessionId={testSessionId}
sessionStatus={testSession.status as 'pending' | 'running' | 'completed' | 'failed'}
autoStart={true}
/>
</div>
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Conversations</h2>
<ConversationsList testSessionId={testSessionId} />
</div>
</div>
);
} catch (err) {
logger.error(`Error fetching test session: ${err}`);
notFound();
}
}
function TestSessionLoading() {
return (
<div className="container mx-auto px-4 py-8">
@ -106,7 +29,87 @@ function TestSessionLoading() {
);
}
export default function TestSessionPage({ params }: PageProps) {
function TestSessionPageContent() {
const params = useParams();
const testSessionId = parseInt(params.id as string);
const { user, loading: authLoading } = useAuth();
const hasFetched = useRef(false);
const [testSession, setTestSession] = useState<TestSessionResponse | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (authLoading || !user || hasFetched.current) return;
hasFetched.current = true;
const fetchTestSession = async () => {
try {
const response = await getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet({
path: {
test_session_id: testSessionId
},
});
if (!response.data) {
setError('Test session not found');
return;
}
setTestSession(response.data);
} catch (err) {
logger.error(`Error fetching test session: ${err}`);
setError('Failed to load test session');
}
};
fetchTestSession();
}, [authLoading, user, testSessionId]);
if (authLoading || (testSession === null && !error)) {
return <TestSessionLoading />;
}
if (error || !testSession) {
return (
<div className="container mx-auto px-4 py-8">
<div className="text-red-500 text-center py-8">
{error || 'Test session not found'}
</div>
</div>
);
}
const sessionForUI = {
id: testSession.id,
name: testSession.name,
description: '',
test_type: testSession.test_index !== null ? 'load_test' : 'single',
status: testSession.status,
actor_workflow_name: `Workflow ${testSession.actor_workflow_id}`,
adversary_workflow_name: `Workflow ${testSession.adversary_workflow_id}`,
created_at: testSession.created_at,
updated_at: testSession.created_at,
test_metadata: testSession.config
};
return (
<div className="container mx-auto px-4 py-8">
<TestSessionDetails session={sessionForUI} />
<TestSessionControls session={sessionForUI} />
<div className="mt-6">
<LiveAudioPlayer
testSessionId={testSessionId}
sessionStatus={testSession.status as 'pending' | 'running' | 'completed' | 'failed'}
autoStart={true}
/>
</div>
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Conversations</h2>
<ConversationsList testSessionId={testSessionId} />
</div>
</div>
);
}
export default function TestSessionPage() {
const backButton = (
<Link href="/looptalk">
<Button variant="ghost" size="sm">
@ -118,9 +121,7 @@ export default function TestSessionPage({ params }: PageProps) {
return (
<LoopTalkLayout backButton={backButton}>
<Suspense fallback={<TestSessionLoading />}>
<PageContent params={params} />
</Suspense>
<TestSessionPageContent />
</LoopTalkLayout>
);
}

View file

@ -9,7 +9,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { NODE_DOCUMENTATION_URLS } from "@/constants/documentation";
import { useAppConfig } from "@/context/AppConfigContext";
import { NodeContent } from "./common/NodeContent";
import { NodeEditDialog } from "./common/NodeEditDialog";
@ -28,7 +27,6 @@ interface TriggerNodeProps extends NodeProps {
export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
const { open, setOpen, handleSaveNodeData, handleDeleteNode } = useNodeHandlers({ id });
const { saveWorkflow } = useWorkflow();
const { config } = useAppConfig();
// Form state
const [name, setName] = useState(data.name || "API Trigger");
@ -36,8 +34,9 @@ export const TriggerNode = memo(({ data, selected, id }: TriggerNodeProps) => {
// Generate trigger_path if not present (should be done on node creation)
const [triggerPath] = useState(() => data.trigger_path ?? crypto.randomUUID());
// Get backend URL from app config (fetched from backend health endpoint)
const backendUrl = config?.backendApiEndpoint || "http://localhost:8000";
const backendUrl =
process.env.NEXT_PUBLIC_BACKEND_URL ||
(typeof window !== 'undefined' ? window.location.origin : '');
const endpoint = `${backendUrl}/api/v1/public/agent/${triggerPath}`;
// Copy state for button feedback

View file

@ -6,7 +6,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useAppConfig } from '@/context/AppConfigContext';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
@ -46,7 +45,6 @@ export function LiveAudioPlayer({
const nextStartTimeRef = useRef(0);
const animationFrameRef = useRef<number | undefined>(undefined);
const isConnectingRef = useRef(false);
const { config } = useAppConfig();
const { user, getAccessToken } = useAuth();
// Auto-start streaming when session starts
@ -99,8 +97,8 @@ export function LiveAudioPlayer({
// Get auth token
const accessToken = await getAccessToken();
// Create WebSocket connection
const baseUrl = (config?.backendApiEndpoint || 'http://localhost:8000').replace(/^http/, 'ws');
const httpBase = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
const baseUrl = httpBase.replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
@ -201,7 +199,7 @@ export function LiveAudioPlayer({
} finally {
isConnectingRef.current = false;
}
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel, config]); // Removed connectionStatus to avoid loops
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel]); // Removed connectionStatus to avoid loops
const disconnect = useCallback(() => {
if (wsRef.current) {

View file

@ -30,7 +30,8 @@ export function SimpleAudioPlayer({ testSessionId }: SimpleAudioPlayerProps) {
const accessToken = await getAccessToken();
// Create WebSocket connection - pass token as query param since WebSocket doesn't support headers
const baseUrl = process.env.NEXT_PUBLIC_API_URL?.replace('http', 'ws') || 'ws://localhost:8000';
const httpBase = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
const baseUrl = httpBase.replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;

View file

@ -2,12 +2,9 @@
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { client } from '@/client/client.gen';
interface AppConfig {
uiVersion: string;
apiVersion: string;
backendApiEndpoint: string | null;
deploymentMode: string;
authProvider: string;
}
@ -20,7 +17,6 @@ interface AppConfigContextType {
const defaultConfig: AppConfig = {
uiVersion: 'dev',
apiVersion: 'unknown',
backendApiEndpoint: null,
deploymentMode: 'oss',
authProvider: 'local',
};
@ -38,15 +34,9 @@ export function AppConfigProvider({ children }: { children: ReactNode }) {
fetch('/api/config/version')
.then((res) => res.json())
.then((data) => {
// Use clientApiBaseUrl (filtered for browser-reachable URLs)
// to configure the API client; keep backendApiEndpoint for display
if (data.clientApiBaseUrl) {
client.setConfig({ baseUrl: data.clientApiBaseUrl });
}
setConfig({
uiVersion: data.ui || 'dev',
apiVersion: data.api || 'unknown',
backendApiEndpoint: data.backendApiEndpoint || null,
deploymentMode: data.deploymentMode || 'oss',
authProvider: data.authProvider || 'local',
});

View file

@ -7,12 +7,9 @@ export const createClientConfig: CreateClientConfig = (config) => {
let baseUrl: string;
if (isServer) {
// for server-side rendering, still use environment variable as fallback
baseUrl = process.env.BACKEND_URL || 'http://api:8000';
} else {
// Client-side API calls are proxied through Next.js rewrites.
// AppConfigContext may update this later with the fetched backend URL.
baseUrl = window.location.origin;
baseUrl = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
}
return {