diff --git a/Dockerfile b/Dockerfile index a68901e4..7d48e15f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -209,9 +209,10 @@ COPY libs ./libs COPY apps/ui ./apps/ui # Build packages in dependency order, then build UI -# VITE_SERVER_URL tells the UI where to find the API server -# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com -ARG VITE_SERVER_URL=http://localhost:3008 +# When VITE_SERVER_URL is empty, the UI uses relative URLs (e.g., /api/...) which nginx proxies +# to the server container. This avoids CORS issues entirely in Docker Compose setups. +# Override at build time if needed: --build-arg VITE_SERVER_URL=http://api.example.com +ARG VITE_SERVER_URL= ENV VITE_SKIP_ELECTRON=true ENV VITE_SERVER_URL=${VITE_SERVER_URL} RUN npm run build:packages && npm run build --workspace=apps/ui diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 0acea6c9..dcd45da8 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -267,6 +267,26 @@ app.use( // CORS configuration // When using credentials (cookies), origin cannot be '*' // We dynamically allow the requesting origin for local development + +// Check if origin is a local/private network address +function isLocalOrigin(origin: string): boolean { + try { + const url = new URL(origin); + const hostname = url.hostname; + return ( + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '[::1]' || + hostname === '0.0.0.0' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') || + /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) + ); + } catch { + return false; + } +} + app.use( cors({ origin: (origin, callback) => { @@ -277,35 +297,25 @@ app.use( } // If CORS_ORIGIN is set, use it (can be comma-separated list) - const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()); - if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') { - if (allowedOrigins.includes(origin)) { - callback(null, origin); - } else { - callback(new Error('Not allowed by CORS')); + const allowedOrigins = process.env.CORS_ORIGIN?.split(',') + .map((o) => o.trim()) + .filter(Boolean); + if (allowedOrigins && allowedOrigins.length > 0) { + if (allowedOrigins.includes('*')) { + callback(null, true); + return; } - return; - } - - // For local development, allow all localhost/loopback origins (any port) - try { - const url = new URL(origin); - const hostname = url.hostname; - - if ( - hostname === 'localhost' || - hostname === '127.0.0.1' || - hostname === '::1' || - hostname === '0.0.0.0' || - hostname.startsWith('192.168.') || - hostname.startsWith('10.') || - hostname.startsWith('172.') - ) { + if (allowedOrigins.includes(origin)) { callback(null, origin); return; } - } catch { - // Ignore URL parsing errors + // Fall through to local network check below + } + + // Allow all localhost/loopback/private network origins (any port) + if (isLocalOrigin(origin)) { + callback(null, origin); + return; } // Reject other origins by default for security diff --git a/apps/ui/nginx.conf b/apps/ui/nginx.conf index 2d96d158..6c50a157 100644 --- a/apps/ui/nginx.conf +++ b/apps/ui/nginx.conf @@ -1,9 +1,28 @@ +# Map for conditional WebSocket upgrade header +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; + # Proxy API and WebSocket requests to the backend server container + # Handles both HTTP API calls and WebSocket upgrades (/api/events, /api/terminal/ws) + location /api/ { + proxy_pass http://server:3008; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $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 $scheme; + } + location / { try_files $uri $uri/ /index.html; } diff --git a/apps/ui/src/components/dialogs/pr-comment-resolution-dialog.tsx b/apps/ui/src/components/dialogs/pr-comment-resolution-dialog.tsx index fa54ffcd..b0f86aa1 100644 --- a/apps/ui/src/components/dialogs/pr-comment-resolution-dialog.tsx +++ b/apps/ui/src/components/dialogs/pr-comment-resolution-dialog.tsx @@ -914,7 +914,7 @@ export function PRCommentResolutionDialog({ {!loading && !error && allComments.length > 0 && ( <> {/* Controls Bar */} -
+
{/* Select All - only interactive when there are visible comments */}
-
+
{/* Show/Hide Resolved Filter Toggle - always visible */}