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 +3 -0
- Dockerfile.backend-dev +9 -0
- README.md +30 -11
- backend/app/config.py +23 -1
- backend/app/main.py +4 -3
- docker-compose.dev.yml +67 -0
- frontend/vite.config.ts +9 -1
- scripts/dev-docker.ps1 +7 -0
.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 |
-
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
2. **Backend** —
|
| 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** —
|
| 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`.
|
| 40 |
|
| 41 |
-
Without
|
| 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://
|
|
|
|
| 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 |
-
|
| 28 |
-
|
|
|
|
| 29 |
|
| 30 |
app = FastAPI(
|
| 31 |
title="CU Student AI Project Helper",
|
|
@@ -58,5 +59,5 @@ async def health():
|
|
| 58 |
}
|
| 59 |
|
| 60 |
|
| 61 |
-
if
|
| 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:
|
| 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 |
+
}
|