NeonClary commited on
Commit
d1f13ff
·
1 Parent(s): 64139df

Add docker-compose.dev.yml hot reload (Mongo, uvicorn --reload, Vite); SERVE_FRONTEND_STATIC; API_PROXY_TARGET

Browse files
.env.example CHANGED
@@ -10,6 +10,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES=10080
10
 
11
  CORS_ORIGINS=http://localhost:5173,http://localhost:3000
12
 
 
 
 
13
  # Optional Google Sheets (service account JSON path or raw JSON in env)
14
  # GOOGLE_SERVICE_ACCOUNT_FILE=
15
  # GOOGLE_SERVICE_ACCOUNT_JSON=
 
10
 
11
  CORS_ORIGINS=http://localhost:5173,http://localhost:3000
12
 
13
+ # Set to false when using Vite dev server (hot reload); API then skips serving frontend/dist.
14
+ # SERVE_FRONTEND_STATIC=true
15
+
16
  # Optional Google Sheets (service account JSON path or raw JSON in env)
17
  # GOOGLE_SERVICE_ACCOUNT_FILE=
18
  # GOOGLE_SERVICE_ACCOUNT_JSON=
Dockerfile.backend-dev ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Python API only — source mounted at runtime for uvicorn --reload (see docker-compose.dev.yml)
2
+ FROM python:3.12-slim-bookworm
3
+ RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
4
+ && rm -rf /var/lib/apt/lists/*
5
+ WORKDIR /app/backend
6
+ COPY backend/requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+ ENV PYTHONPATH=/app/backend
9
+ EXPOSE 8000
README.md CHANGED
@@ -8,27 +8,45 @@ Recruitment and tooling site for **non-technical** CU students joining a Collabo
8
  - **Backend:** FastAPI, Motor (MongoDB), Google Gemini API (`GEMINI_API_KEY`)
9
  - **Deploy:** Docker (single image: API + static SPA); suitable for Hugging Face Spaces or any container host
10
 
11
- ## Local development
12
 
13
- 1. **MongoDB**install locally or use Docker only for DB:
14
 
15
- ```bash
16
- docker compose up mongo
17
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- 2. **Backend** — from repo root:
20
 
21
  ```bash
22
  cd backend
23
  python -m venv .venv
24
  .venv\Scripts\activate # Windows
25
  pip install -r requirements.txt
26
- copy ..\.env.example .env # or copy to repo root; config loads either
27
- # Set GEMINI_API_KEY, JWT_SECRET_KEY, MONGODB_URL=mongodb://localhost:27017
28
  uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
29
  ```
30
 
31
- 3. **Frontend** — separate terminal:
32
 
33
  ```bash
34
  cd frontend
@@ -36,9 +54,9 @@ Recruitment and tooling site for **non-technical** CU students joining a Collabo
36
  npm run dev
37
  ```
38
 
39
- Vite proxies `/api` to `http://127.0.0.1:8000`. Open `http://localhost:5173`.
40
 
41
- Without a built `frontend/dist`, the API still serves OpenAPI at `/docs`. After `npm run build`, run uvicorn from repo root context so `frontend/dist` exists — or use Docker below.
42
 
43
  ## Configuration
44
 
@@ -51,6 +69,7 @@ See [.env.example](.env.example). Important variables:
51
  | `MONGODB_URL` / `MONGODB_DB` | User accounts and saved drafts |
52
  | `JWT_SECRET_KEY` | Sign auth tokens |
53
  | `CORS_ORIGINS` | Frontend origins (dev: `http://localhost:5173`) |
 
54
  | `GOOGLE_*` / SMTP | Optional submission pipeline (see `app/routers/submit.py`) |
55
 
56
  **Speech:** Mic input is sent to `POST /api/transcribe` (Gemini multimodal). “Read aloud” uses the browser **Web Speech API** (no extra key).
 
8
  - **Backend:** FastAPI, Motor (MongoDB), Google Gemini API (`GEMINI_API_KEY`)
9
  - **Deploy:** Docker (single image: API + static SPA); suitable for Hugging Face Spaces or any container host
10
 
11
+ ## Local development (hot reload)
12
 
13
+ ### Option A Docker Compose (matches other Neon demos: Mongo + API reload + Vite HMR)
14
 
15
+ From the repo root, with `.env` configured (at least `GEMINI_API_KEY`, `JWT_SECRET_KEY`):
16
+
17
+ ```bash
18
+ docker compose -f docker-compose.dev.yml up --build
19
+ ```
20
+
21
+ | Service | URL | Notes |
22
+ |--------|-----|--------|
23
+ | **UI (Vite)** | http://localhost:5173 | Hot reload (HMR); proxies `/api` → backend |
24
+ | **API** | http://localhost:8000 | `uvicorn --reload`; OpenAPI at http://localhost:8000/docs |
25
+ | **MongoDB** | localhost:27017 | Data volume `cu_student_mongo_dev` |
26
+
27
+ The dev stack sets `SERVE_FRONTEND_STATIC=false` so FastAPI does **not** serve `frontend/dist` and you always use the Vite dev server. The frontend container sets `CHOKIDAR_USEPOLLING=true` so file watches work on Docker Desktop (Windows/macOS).
28
+
29
+ PowerShell:
30
+
31
+ ```powershell
32
+ docker compose -f docker-compose.dev.yml up --build
33
+ ```
34
+
35
+ ### Option B — Processes on your machine (no API container)
36
+
37
+ 1. **MongoDB** — e.g. `docker compose -f docker-compose.dev.yml up mongo -d` or a local MongoDB on `27017`.
38
 
39
+ 2. **Backend** — repo root `.env` should include `SERVE_FRONTEND_STATIC=false` (or omit `frontend/dist` so static is not mounted). Then:
40
 
41
  ```bash
42
  cd backend
43
  python -m venv .venv
44
  .venv\Scripts\activate # Windows
45
  pip install -r requirements.txt
 
 
46
  uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
47
  ```
48
 
49
+ 3. **Frontend** — second terminal:
50
 
51
  ```bash
52
  cd frontend
 
54
  npm run dev
55
  ```
56
 
57
+ Open http://localhost:5173 — Vite proxies `/api` to `http://127.0.0.1:8000`.
58
 
59
+ Without mounting static, the API exposes OpenAPI at `/docs` on port 8000.
60
 
61
  ## Configuration
62
 
 
69
  | `MONGODB_URL` / `MONGODB_DB` | User accounts and saved drafts |
70
  | `JWT_SECRET_KEY` | Sign auth tokens |
71
  | `CORS_ORIGINS` | Frontend origins (dev: `http://localhost:5173`) |
72
+ | `SERVE_FRONTEND_STATIC` | `true` (default): serve `frontend/dist` from FastAPI. `false`: use Vite dev server only (hot reload). |
73
  | `GOOGLE_*` / SMTP | Optional submission pipeline (see `app/routers/submit.py`) |
74
 
75
  **Speech:** Mic input is sent to `POST /api/transcribe` (Gemini multimodal). “Read aloud” uses the browser **Web Speech API** (no extra key).
backend/app/config.py CHANGED
@@ -1,5 +1,7 @@
1
  from pathlib import Path
 
2
 
 
3
  from pydantic_settings import BaseSettings, SettingsConfigDict
4
 
5
  # backend/app/config.py -> parents: app, backend, repo root
@@ -27,10 +29,30 @@ class Settings(BaseSettings):
27
  access_token_expire_minutes: int = 60 * 24 * 7
28
 
29
  cors_origins: str = (
30
- "http://localhost:5173,http://localhost:3000,"
 
31
  "http://localhost:7860,http://127.0.0.1:7860"
32
  )
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  google_service_account_file: str | None = None
35
  google_service_account_json: str | None = None
36
  google_spreadsheet_id: str | None = None
 
1
  from pathlib import Path
2
+ from typing import Any
3
 
4
+ from pydantic import field_validator
5
  from pydantic_settings import BaseSettings, SettingsConfigDict
6
 
7
  # backend/app/config.py -> parents: app, backend, repo root
 
29
  access_token_expire_minutes: int = 60 * 24 * 7
30
 
31
  cors_origins: str = (
32
+ "http://localhost:5173,http://127.0.0.1:5173,"
33
+ "http://localhost:3000,"
34
  "http://localhost:7860,http://127.0.0.1:7860"
35
  )
36
 
37
+ # When false, API does not mount frontend/dist — use Vite dev server (hot reload) on :5173.
38
+ serve_frontend_static: bool = True
39
+
40
+ @field_validator("serve_frontend_static", mode="before")
41
+ @classmethod
42
+ def _coerce_serve_static(cls, v: Any) -> bool:
43
+ if isinstance(v, bool):
44
+ return v
45
+ if v is None:
46
+ return True
47
+ if isinstance(v, str):
48
+ s = v.strip().lower()
49
+ if s in ("0", "false", "no", "off"):
50
+ return False
51
+ if s in ("1", "true", "yes", "on"):
52
+ return True
53
+ return True
54
+ return bool(v)
55
+
56
  google_service_account_file: str | None = None
57
  google_service_account_json: str | None = None
58
  google_spreadsheet_id: str | None = None
backend/app/main.py CHANGED
@@ -24,8 +24,9 @@ async def lifespan(app: FastAPI):
24
  get_client().close()
25
 
26
 
27
- _docs = None if FRONTEND_DIST.is_dir() else "/docs"
28
- _redoc = None if FRONTEND_DIST.is_dir() else "/redoc"
 
29
 
30
  app = FastAPI(
31
  title="CU Student AI Project Helper",
@@ -58,5 +59,5 @@ async def health():
58
  }
59
 
60
 
61
- if FRONTEND_DIST.is_dir():
62
  app.mount("/", StaticFiles(directory=str(FRONTEND_DIST), html=True), name="static")
 
24
  get_client().close()
25
 
26
 
27
+ _should_mount_static = settings.serve_frontend_static and FRONTEND_DIST.is_dir()
28
+ _docs = None if _should_mount_static else "/docs"
29
+ _redoc = None if _should_mount_static else "/redoc"
30
 
31
  app = FastAPI(
32
  title="CU Student AI Project Helper",
 
59
  }
60
 
61
 
62
+ if _should_mount_static:
63
  app.mount("/", StaticFiles(directory=str(FRONTEND_DIST), html=True), name="static")
docker-compose.dev.yml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hot reload: Mongo + FastAPI (uvicorn --reload) + Vite dev server.
2
+ # Usage: docker compose -f docker-compose.dev.yml up
3
+ # App UI: http://localhost:5173 | API: http://localhost:8000 | /docs when static is off
4
+
5
+ name: cu-student-helper-dev
6
+
7
+ services:
8
+ mongo:
9
+ image: mongo:7
10
+ ports:
11
+ - "27017:27017"
12
+ volumes:
13
+ - cu_student_mongo_dev:/data/db
14
+
15
+ api:
16
+ build:
17
+ context: .
18
+ dockerfile: Dockerfile.backend-dev
19
+ ports:
20
+ - "8000:8000"
21
+ env_file:
22
+ - .env
23
+ environment:
24
+ MONGODB_URL: mongodb://mongo:27017
25
+ SERVE_FRONTEND_STATIC: "false"
26
+ CORS_ORIGINS: http://localhost:5173,http://127.0.0.1:5173,http://localhost:8000,http://127.0.0.1:8000
27
+ volumes:
28
+ - ./backend:/app/backend
29
+ command:
30
+ [
31
+ "uvicorn",
32
+ "app.main:app",
33
+ "--host",
34
+ "0.0.0.0",
35
+ "--port",
36
+ "8000",
37
+ "--reload",
38
+ "--reload-dir",
39
+ "/app/backend/app",
40
+ ]
41
+ depends_on:
42
+ - mongo
43
+
44
+ frontend:
45
+ image: node:20-bookworm
46
+ working_dir: /app/frontend
47
+ ports:
48
+ - "5173:5173"
49
+ volumes:
50
+ - ./frontend:/app/frontend
51
+ - frontend_node_modules:/app/frontend/node_modules
52
+ environment:
53
+ CHOKIDAR_USEPOLLING: "true"
54
+ WATCHPACK_POLLING: "true"
55
+ API_PROXY_TARGET: http://api:8000
56
+ command:
57
+ [
58
+ "sh",
59
+ "-c",
60
+ "test -d node_modules/react || npm ci; npm run dev -- --host 0.0.0.0 --port 5173",
61
+ ]
62
+ depends_on:
63
+ - api
64
+
65
+ volumes:
66
+ cu_student_mongo_dev:
67
+ frontend_node_modules:
frontend/vite.config.ts CHANGED
@@ -1,13 +1,21 @@
1
  import { defineConfig } from 'vite'
2
  import react from '@vitejs/plugin-react'
3
 
 
 
 
 
4
  export default defineConfig({
5
  plugins: [react()],
6
  server: {
7
  port: 5173,
 
 
 
 
8
  proxy: {
9
  '/api': {
10
- target: 'http://127.0.0.1:8000',
11
  changeOrigin: true,
12
  },
13
  },
 
1
  import { defineConfig } from 'vite'
2
  import react from '@vitejs/plugin-react'
3
 
4
+ // In Docker Compose dev, API_PROXY_TARGET=http://api:8000 (Vite proxies /api → backend).
5
+ // Local (no Docker): default http://127.0.0.1:8000
6
+ const apiProxy = process.env.API_PROXY_TARGET || 'http://127.0.0.1:8000'
7
+
8
  export default defineConfig({
9
  plugins: [react()],
10
  server: {
11
  port: 5173,
12
+ strictPort: true,
13
+ watch: {
14
+ usePolling: process.env.CHOKIDAR_USEPOLLING === 'true',
15
+ },
16
  proxy: {
17
  '/api': {
18
+ target: apiProxy,
19
  changeOrigin: true,
20
  },
21
  },
scripts/dev-docker.ps1 ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Hot-reload stack: Mongo + uvicorn --reload + Vite — http://localhost:5173
2
+ Set-Location (Join-Path $PSScriptRoot '..')
3
+ if ($args.Count -eq 0) {
4
+ docker compose -f docker-compose.dev.yml up --build
5
+ } else {
6
+ docker compose -f docker-compose.dev.yml @args
7
+ }