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* ./
RUN npm install
COPY . .
ARG VITE_API_BASE=/api
ARG VITE_WIZARD_PORT=18080
ENV VITE_API_BASE=$VITE_API_BASE
ENV VITE_WIZARD_PORT=$VITE_WIZARD_PORT
# API and wizard are proxied via nginx — no VITE_ env vars needed
RUN npm run build
# Production stage — lightweight static server, no nginx
FROM node:20-alpine
RUN npm install -g serve@14
WORKDIR /app
COPY --from=build /app/dist ./dist
# Production stage — nginx with reverse proxy
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
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 api from '@/services/api'
const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080
type AppState = 'checking' | 'setup' | 'ready'
export default function App() {
@@ -41,7 +39,7 @@ export default function App() {
// Backend not ready — show setup wizard
if (appState === 'setup') {
return <SetupWizardPage wizardPort={WIZARD_PORT} onComplete={checkBackend} />
return <SetupWizardPage wizardPort={18080} onComplete={checkBackend} />
}
// 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-label"> Issues</span>
</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' }}>
<span className="stat-number">{v}</span>
<span className="stat-label">{k}</span>
@@ -39,7 +39,7 @@ export default function DashboardPage() {
<div className="section">
<h3></h3>
<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}>
<span className="bar-label">{k}</span>
<div className="bar" style={{

View File

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

View File

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