fix: nginx reverse proxy for API/wizard, fix Object.entries null crash

- Replace serve with nginx for proper reverse proxy
- /api/* proxied to backend:8000, /wizard/* proxied to wizard:8080
- Eliminates CORS issues (same-origin requests)
- Fixes SPA fallback returning 200 for API routes (was hiding backend-down state)
- Add null guards on Object.entries for dashboard stats
- Remove VITE_API_BASE/VITE_WIZARD_PORT build args (no longer needed)
This commit is contained in:
zhi
2026-03-11 09:43:06 +00:00
parent f8fac48fcc
commit a655e41822
6 changed files with 40 additions and 17 deletions

View File

@@ -4,16 +4,12 @@ WORKDIR /app
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
RUN npm install RUN npm install
COPY . . COPY . .
ARG VITE_API_BASE=/api # API and wizard are proxied via nginx — no VITE_ env vars needed
ARG VITE_WIZARD_PORT=18080
ENV VITE_API_BASE=$VITE_API_BASE
ENV VITE_WIZARD_PORT=$VITE_WIZARD_PORT
RUN npm run build RUN npm run build
# Production stage — lightweight static server, no nginx # Production stage — nginx with reverse proxy
FROM node:20-alpine FROM nginx:alpine
RUN npm install -g serve@14 COPY --from=build /app/dist /usr/share/nginx/html
WORKDIR /app COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist ./dist
EXPOSE 3000 EXPOSE 3000
CMD ["serve", "-s", "dist", "-l", "3000"] CMD ["nginx", "-g", "daemon off;"]

29
nginx.conf Normal file
View File

@@ -0,0 +1,29 @@
server {
listen 3000;
root /usr/share/nginx/html;
index index.html;
# Backend API proxy
location /api/ {
proxy_pass http://backend:8000/;
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_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Wizard API proxy (setup phase only)
location /wizard/ {
proxy_pass http://wizard:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 10s;
}
# SPA fallback — only for non-API, non-asset routes
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -15,8 +15,6 @@ import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
import NotificationsPage from '@/pages/NotificationsPage' import NotificationsPage from '@/pages/NotificationsPage'
import api from '@/services/api' import api from '@/services/api'
const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080
type AppState = 'checking' | 'setup' | 'ready' type AppState = 'checking' | 'setup' | 'ready'
export default function App() { export default function App() {
@@ -41,7 +39,7 @@ export default function App() {
// Backend not ready — show setup wizard // Backend not ready — show setup wizard
if (appState === 'setup') { if (appState === 'setup') {
return <SetupWizardPage wizardPort={WIZARD_PORT} onComplete={checkBackend} /> return <SetupWizardPage wizardPort={18080} onComplete={checkBackend} />
} }
// Backend ready but auth loading // Backend ready but auth loading

View File

@@ -28,7 +28,7 @@ export default function DashboardPage() {
<span className="stat-number">{stats.total_issues}</span> <span className="stat-number">{stats.total_issues}</span>
<span className="stat-label"> Issues</span> <span className="stat-label"> Issues</span>
</div> </div>
{Object.entries(stats.by_status).map(([k, v]) => ( {Object.entries(stats.by_status || {}).map(([k, v]) => (
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}> <div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
<span className="stat-number">{v}</span> <span className="stat-number">{v}</span>
<span className="stat-label">{k}</span> <span className="stat-label">{k}</span>
@@ -39,7 +39,7 @@ export default function DashboardPage() {
<div className="section"> <div className="section">
<h3></h3> <h3></h3>
<div className="bar-chart"> <div className="bar-chart">
{Object.entries(stats.by_priority).map(([k, v]) => ( {Object.entries(stats.by_priority || {}).map(([k, v]) => (
<div className="bar-row" key={k}> <div className="bar-row" key={k}>
<span className="bar-label">{k}</span> <span className="bar-label">{k}</span>
<div className="bar" style={{ <div className="bar" style={{

View File

@@ -48,7 +48,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
}) })
const wizardApi = axios.create({ const wizardApi = axios.create({
baseURL: `http://127.0.0.1:${wizardPort}`, baseURL: '/wizard',
timeout: 5000, timeout: 5000,
}) })

View File

@@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
const api = axios.create({ const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE || '/api', baseURL: '/api',
}) })
api.interceptors.request.use((config) => { api.interceptors.request.use((config) => {