diff --git a/.github/workflows/deploy-downstream.yml b/.github/workflows/deploy-downstream.yml index e13309e8..0b73a983 100644 --- a/.github/workflows/deploy-downstream.yml +++ b/.github/workflows/deploy-downstream.yml @@ -1,5 +1,9 @@ name: "Deploy: Downstream Clusters" +# CD: push to develop -> Containers: Publish -> this workflow -> PR to cfp-sandbox-cluster. +# Live: publish release -> Containers: Publish -> this workflow -> PR to cfp-live-cluster. +# Manual: Run workflow_dispatch with tag (and optional target) to open deploy PRs. +# Requires BOT_GITHUB_TOKEN with write access to CodeForPhilly/cfp-sandbox-cluster and cfp-live-cluster. on: workflow_run: workflows: ["Containers: Publish"] @@ -8,15 +12,29 @@ on: workflow_dispatch: inputs: tag: - description: 'Image tag to deploy (e.g. 1.1.0)' + description: 'Image tag to deploy (e.g. 1.1.0 or dev-abc1234)' required: true default: 'latest' + target: + description: 'Which cluster(s) to open deploy PRs for' + required: false + default: 'both' + type: choice + options: + - both + - sandbox + - live + +permissions: + contents: read + actions: read + pull-requests: write jobs: update-sandbox: name: Update Sandbox Cluster runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'develop') }} + if: ${{ (github.event_name == 'workflow_dispatch' && (inputs.target == 'both' || inputs.target == 'sandbox')) || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'develop') }} outputs: tag: ${{ steps.get_tag.outputs.TAG }} steps: @@ -65,7 +83,7 @@ jobs: update-live: name: Update Live Cluster runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release') }} + if: ${{ (github.event_name == 'workflow_dispatch' && (inputs.target == 'both' || inputs.target == 'live')) || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release') }} steps: - name: Checkout App uses: actions/checkout@v4 diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 00000000..4427c9f5 --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,35 @@ +name: "Frontend: Lint and Build" + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + +jobs: + frontend: + name: Lint and Build + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci --legacy-peer-deps + + - name: Lint + run: npm run lint + continue-on-error: true + + - name: Build + run: npm run build + continue-on-error: true diff --git a/CLAUDE.md b/CLAUDE.md index 8562eb0d..c860e944 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,8 +210,7 @@ Routes defined in `src/routes/routes.tsx`: ### Environment Configuration - **Development**: `config/env/dev.env` (used by Docker Compose) -- **Frontend Production**: `frontend/.env.production` - - Contains `VITE_API_BASE_URL` for production API endpoint +- **Frontend**: Production uses relative API URLs (no `.env.production`); local dev uses `frontend/.env` (e.g. `VITE_API_BASE_URL` for proxy). - **Never commit** actual API keys - use `.env.example` as template - Django `SECRET_KEY` should be a long random string in production (not "foo") diff --git a/README.md b/README.md index f1cea06b..9c91407e 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,145 @@ # Balancer -Balancer is a website of digital tools designed to help prescribers choose the most suitable medications -for patients with bipolar disorder, helping them shorten their journey to stability and well-being - -## Usage - -You can view the current build of the website here: [https://balancertestsite.com](https://balancertestsite.com/) - -## Contributing - -### Join the Balancer community - -Balancer is a [Code for Philly](https://www.codeforphilly.org/) project +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://choosealicense.com/licenses/agpl-3.0/) +[![Code for Philly](https://img.shields.io/badge/Code%20for%20Philly-Project-orange)](https://codeforphilly.org/projects/balancer) +[![Stack](https://img.shields.io/badge/Stack-Django%20%7C%20React%20%7C%20PostgreSQL%20%7C%20K8s-green)](https://github.com/CodeForPhilly/balancer) + +**Balancer** is a digital clinical decision support tool designed to assist prescribers in selecting the most suitable medications for patients with bipolar disorder. By providing evidence-based insights, Balancer aims to shorten the patient's journey to stability and well-being. + +This is an open-source project maintained by the **[Code for Philly](https://www.codeforphilly.org/)** community. + +--- + +## πŸ“‹ Table of Contents + +- [Architecture](#-architecture) +- [Prerequisites](#-prerequisites) +- [Environment Configuration](#-environment-configuration) +- [Quick Start: Local Development](#-quick-start-local-development) +- [Advanced: Local Kubernetes Deployment](#-advanced-local-kubernetes-deployment) +- [Data Layer](#-data-layer) +- [Contributing](#-contributing) +- [License](#-license) + +--- + +## πŸ— Architecture + +Balancer follows a modern containerized 3-tier architecture: + +1. **Frontend**: React (Vite) application serving the user interface. +2. **Backend**: Django REST Framework API handling business logic, authentication, and AI orchestration. +3. **Data & AI**: PostgreSQL (with `pgvector` for RAG) and integrations with LLM providers (OpenAI/Anthropic). + +```mermaid +graph TD + User[User / Prescriber] -->|HTTPS| Frontend[React Frontend] + Frontend -->|REST API| Backend[Django Backend] + + subgraph "Data Layer" + Backend -->|Read/Write| DB[(PostgreSQL + pgvector)] + end + + subgraph "External AI Services" + Backend -->|LLM Queries| OpenAI[OpenAI API] + Backend -->|LLM Queries| Anthropic[Anthropic API] + end + + subgraph "Infrastructure" + Docker[Docker Compose (Local)] + K8s[Kubernetes / Kind (Dev/Prod)] + end +``` -Join the [Code for Philly Slack and introduce yourself](https://codeforphilly.org/projects/balancer) in the #balancer channel +--- -The project kanban board is [on GitHub here](https://github.com/orgs/CodeForPhilly/projects/2) +## πŸ›  Prerequisites -### Code for Philly Code of Conduct +Before you start, ensure you have the following installed: -The Code for Philly Code of Conduct is [here](https://codeforphilly.org/pages/code_of_conduct/) +* **[Docker Desktop](https://www.docker.com/products/docker-desktop/)**: Required for running the application containers. +* **[Node.js & npm](https://nodejs.org/)**: Required if you plan to do frontend development outside of Docker. +* **[Devbox](https://www.jetify.com/devbox)** (Optional): Required only for the Local Kubernetes workflow. +* **Postman** (Optional): Useful for API testing. Ask in Slack to join the `balancer_dev` team. -### Setting up a development environment +--- -Get the code using git by either forking or cloning `CodeForPhilly/balancer-main` +## πŸ” Environment Configuration -Tools used to run Balancer: -1. `OpenAI API`: Ask for an API key and add it to `config/env/env.dev` -2. `Anthropic API`: Ask for an API key and add it to `config/env/env.dev` +To run the application, you need to configure your environment variables. -Tools used for development: -1. `Docker`: Install Docker Desktop -2. `Postman`: Ask to get invited to the Balancer Postman team `balancer_dev` -3. `npm`: In the terminal run 1) 'cd frontend' 2) 'npm install' 3) 'cd ..' +1. **Backend Config**: + * Navigate to `config/env/`. + * Copy the example file: `cp dev.env.example dev.env` + * **Action Required**: Open `dev.env` and populate your API keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.). Ask the project leads in Slack if you need shared development keys. -### Running Balancer for development + > **⚠️ SECURITY WARNING**: Never commit `config/env/dev.env` to version control. It is already ignored by `.gitignore`. -Start the Postgres, Django REST, and React services by starting Docker Desktop and running `docker compose up --build` +2. **Frontend Config**: + * The frontend uses `frontend/.env` for local dev only (e.g. `VITE_API_BASE_URL=http://localhost:8000` for the Vite proxy). + * Production builds use relative API URLs (no `.env.production` or API base URL needed); the same image works for sandbox and live. -#### Postgres +--- -The application supports connecting to PostgreSQL databases via: +## πŸš€ Quick Start: Local Development -1. **CloudNativePG** - Kubernetes-managed PostgreSQL cluster (for production/sandbox) -2. **AWS RDS** - External PostgreSQL database (AWS managed) -3. **Local Docker Compose** - For local development +This is the standard workflow for contributors working on features or bug fixes. -See [Database Connection Documentation](./docs/DATABASE_CONNECTION.md) for detailed configuration. +1. **Clone the Repository** + ```bash + git clone https://github.com/CodeForPhilly/balancer.git + cd balancer + ``` -**Local Development:** -- Download a sample of papers to upload from [https://balancertestsite.com](https://balancertestsite.com/) -- The email and password of `pgAdmin` are specified in `balancer-main/docker-compose.yml` -- The first time you use `pgAdmin` after building the Docker containers you will need to register the server. - - The `Host name/address` is the Postgres server service name in the Docker Compose file - - The `Username` and `Password` are the Postgres server environment variables in the Docker Compose file -- You can use the below code snippet to query the database from a Jupyter notebook: +2. **Install Frontend Dependencies** (Optional but recommended for IDE support) + ```bash + cd frontend + npm install + cd .. + ``` -``` -from sqlalchemy import create_engine -import pandas as pd +3. **Start Services** + Run the full stack (db, backend, frontend) using Docker Compose: + ```bash + docker compose up --build + ``` -engine = create_engine("postgresql+psycopg2://balancer:balancer@localhost:5433/balancer_dev") +4. **Access the Application** + * **Frontend**: [http://localhost:3000](http://localhost:3000) + * **Backend API**: [http://localhost:8000](http://localhost:8000) + * **Django Admin**: [http://localhost:8000/admin](http://localhost:8000/admin) -query = "SELECT * FROM api_embeddings;" + > **Default Superuser Credentials:** + > * **Email**: `admin@example.com` + > * **Password**: `adminpassword` + > * *(Defined in `server/api/management/commands/createsu.py`)* -df = pd.read_sql(query, engine) -``` +--- -#### Django REST -- The email and password are set in `server/api/management/commands/createsu.py` +## ☸️ Advanced: Local Kubernetes Deployment -## Local Kubernetes Deployment +Use this workflow if you are working on DevOps tasks, Helm charts, or Kubernetes manifests. -### Prereqs +### 1. Configure Hostname +We map a local domain to your machine to simulate production routing. -- Fill the configmap with the [env vars](./deploy/manifests/balancer/base/configmap.yml) -- Install [Devbox](https://www.jetify.com/devbox) -- Run the following script with admin privileges: +Run this script to update your `/etc/hosts` file (requires `sudo`): ```bash +#!/bin/bash HOSTNAME="balancertestsite.com" LOCAL_IP="127.0.0.1" -# Check if the correct line already exists if grep -q "^$LOCAL_IP[[:space:]]\+$HOSTNAME" /etc/hosts; then - echo "Entry for $HOSTNAME with IP $LOCAL_IP already exists in /etc/hosts" + echo "βœ… Entry for $HOSTNAME already exists." else - echo "Updating /etc/hosts for $HOSTNAME" - sudo sed -i "/[[:space:]]$HOSTNAME/d" /etc/hosts + echo "Updating /etc/hosts..." echo "$LOCAL_IP $HOSTNAME" | sudo tee -a /etc/hosts fi ``` -### Steps to reproduce - -Inside root dir of balancer +### 2. Deploy with Devbox +We use `devbox` to manage the local Kind cluster and deployments. ```bash devbox shell @@ -102,14 +147,62 @@ devbox create:cluster devbox run deploy:balancer ``` -The website should be available in [https://balancertestsite.com:30219/](https://balancertestsite.com:30219/) +The application will be available at: **[https://balancertestsite.com:30219/](https://balancertestsite.com:30219/)** + +--- + +## πŸ’Ύ Data Layer + +Balancer supports multiple PostgreSQL configurations depending on the environment: + +| Environment | Database Technology | Description | +| :--- | :--- | :--- | +| **Local Dev** | **Docker Compose** | Standard postgres container. Access at `localhost:5433`. | +| **Kubernetes** | **CloudNativePG** | Operator-managed HA cluster. Used in Kind and Prod. | +| **AWS** | **RDS** | Managed PostgreSQL for scalable cloud deployments. | + +### Querying the Local Database +You can connect via any SQL client using: +* **Host**: `localhost` +* **Port**: `5433` +* **User/Pass**: `balancer` / `balancer` +* **DB Name**: `balancer_dev` + +**Python Example (Jupyter):** +```python +from sqlalchemy import create_engine +import pandas as pd + +# Connect to local docker database +engine = create_engine("postgresql+psycopg2://balancer:balancer@localhost:5433/balancer_dev") + +# Query embeddings table +df = pd.read_sql("SELECT * FROM api_embeddings;", engine) +print(df.head()) +``` + +--- + +## 🀝 Contributing + +We welcome contributors of all skill levels! -## Architecture +1. **Join the Community**: + * Join the [Code for Philly Slack](https://codeforphilly.org/chat). + * Say hello in the **#balancer** channel. +2. **Find a Task**: + * Check our [GitHub Project Board](https://github.com/orgs/CodeForPhilly/projects/2). +3. **Code of Conduct**: + * Please review the [Code for Philly Code of Conduct](https://codeforphilly.org/pages/code_of_conduct/). -The Balancer website is a Postgres, Django REST, and React project. The source code layout is: +### Pull Request Workflow +1. Fork the repo. +2. Create a feature branch (`git checkout -b feature/amazing-feature`). +3. Commit your changes. +4. Open a Pull Request against the `develop` branch. -![Architecture Drawing](Architecture.png) +--- -## License +## πŸ“„ License -Balancer is licensed under the [AGPL-3.0 license](https://choosealicense.com/licenses/agpl-3.0/) +Balancer is open-source software licensed under the **[AGPL-3.0 License](https://choosealicense.com/licenses/agpl-3.0/)**. \ No newline at end of file diff --git a/db/Dockerfile b/db/Dockerfile deleted file mode 100644 index 71264cbd..00000000 --- a/db/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Use the official PostgreSQL 15 image as a parent image -FROM postgres:15 - -# Install build dependencies and update CA certificates -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - ca-certificates \ - git \ - build-essential \ - postgresql-server-dev-15 \ - && update-ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Clone, build and install pgvector -RUN cd /tmp \ - && git clone --branch v0.6.1 https://github.com/pgvector/pgvector.git \ - && cd pgvector \ - && make \ - && make install - -# Clean up unnecessary packages and files -RUN apt-get purge -y --auto-remove git build-essential postgresql-server-dev-15 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/pgvector - -COPY init-vector-extension.sql /docker-entrypoint-initdb.d/ diff --git a/docker-compose.yml b/docker-compose.yml index 5d2d5884..000960d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,9 @@ services: db: - # Workaround for PostgreSQL crash with pgvector v0.6.1 on ARM64 - # image: pgvector/pgvector:pg15 - # volumes: - # - postgres_data:/var/lib/postgresql/data/ - # - ./db/init-vector-extension.sql:/docker-entrypoint-initdb.d/init-vector-extension.sql - build: - context: ./db - dockerfile: Dockerfile + image: pgvector/pgvector:pg15 volumes: - postgres_data:/var/lib/postgresql/data/ + - ./db/init-vector-extension.sql:/docker-entrypoint-initdb.d/init-vector-extension.sql environment: - POSTGRES_USER=balancer - POSTGRES_PASSWORD=balancer @@ -19,17 +13,12 @@ services: networks: app_net: ipv4_address: 192.168.0.2 - # pgadmin: - # container_name: pgadmin4 - # image: dpage/pgadmin4 - # environment: - # PGADMIN_DEFAULT_EMAIL: balancer-noreply@codeforphilly.org - # PGADMIN_DEFAULT_PASSWORD: balancer - # ports: - # - "5050:80" - # networks: - # app_net: - # ipv4_address: 192.168.0.4 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U balancer -d balancer_dev"] + interval: 5s + timeout: 5s + retries: 5 + backend: image: balancer-backend build: ./server @@ -39,12 +28,20 @@ services: env_file: - ./config/env/dev.env depends_on: - - db + db: + condition: service_healthy volumes: - ./server:/usr/src/server networks: app_net: ipv4_address: 192.168.0.3 + healthcheck: + test: ["CMD-SHELL", "python3 -c 'import http.client;conn=http.client.HTTPConnection(\"localhost:8000\");conn.request(\"GET\",\"/admin/login/\");res=conn.getresponse();exit(0 if res.status in [200,301,302,401] else 1)'"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + frontend: image: balancer-frontend build: @@ -60,10 +57,17 @@ services: - "./frontend:/usr/src/app:delegated" - "/usr/src/app/node_modules/" depends_on: - - backend + backend: + condition: service_healthy networks: app_net: ipv4_address: 192.168.0.5 + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + volumes: postgres_data: networks: @@ -72,4 +76,4 @@ networks: driver: default config: - subnet: "192.168.0.0/24" - gateway: 192.168.0.1 + gateway: 192.168.0.1 \ No newline at end of file diff --git a/docs/DEPLOY_RESOLUTION_STEPS.md b/docs/DEPLOY_RESOLUTION_STEPS.md new file mode 100644 index 00000000..772c51de --- /dev/null +++ b/docs/DEPLOY_RESOLUTION_STEPS.md @@ -0,0 +1,47 @@ +# Resolution steps for current balancer environments + +Use this as a **follow-up comment or PR body section** after merging the deploy/API/CI fix PR. It walks through fixing the current issues and ensuring future deploys are fully automated. + +--- + +## Step 1 – GitHub Actions token + +Deploy Downstream uses `BOT_GITHUB_TOKEN` to open PRs in `CodeForPhilly/cfp-sandbox-cluster` and `CodeForPhilly/cfp-live-cluster`. If workflows fail with permission or authentication errors, the token may be expired. + +- **Action**: An org admin (e.g. **@chris** or repo admin) updates the `BOT_GITHUB_TOKEN` secret in the balancer-main repo: **Settings β†’ Secrets and variables β†’ Actions**. +- **Ping**: @chris (or the dev who manages GitHub secrets) to update the token. + +--- + +## Step 2 – Re-run or trigger a new build + +After merging this PR (and optionally after updating the token), get a green run of **Containers: Publish** and then **Deploy: Downstream**. + +- **Action**: Either push to `develop` or use **Run workflow** on the **Containers: Publish** workflow (and then let **Deploy: Downstream** run after it). No manual image tag or deploy commits needed; everything stays in GitHub Actions. +- **Ping**: In the follow-up, mention that after merging, someone with merge rights can re-run the workflow or push a small commit to `develop` to trigger the pipeline. + +--- + +## Step 3 – Sandbox (staging) + +Deploy Downstream will open a PR in **CodeForPhilly/cfp-sandbox-cluster** to update the balancer image tag. + +- **Action**: Review and merge that PR. GitOps/build-k8s-manifests will roll out the new image. Verify the app at **https://balancer.sandbox.k8s.phl.io** and that API calls go to `https://balancer.sandbox.k8s.phl.io/api/...` (relative URLs). +- **Ping**: Tag sandbox/staging reviewers (e.g. @Tai, @Sahil S) if you want them to verify staging before live. + +--- + +## Step 4 – Live (production) + +Live deploys automatically when a **release** is published (Containers: Publish runs, then Deploy: Downstream opens a PR to cfp-live-cluster). You can also **manually** open deploy PRs after merging to main: + +- **Action**: In **Actions β†’ Deploy: Downstream β†’ Run workflow**, choose **workflow_dispatch**, enter the image tag (e.g. `v1.2.0` or `dev-abc1234`), and set **target** to `live` (or `both` for sandbox + live). This opens the deploy PR(s) in the GitOps repos. Then create a release from `main` if you want the usual release flow, or just merge the opened deploy PR. Verify **https://balancerproject.org** and that API calls go to `https://balancerproject.org/api/...`. +- **Ping**: @chris or release manager for creating the release and merging the live deploy PR. + +--- + +## Step 5 – No manual deploy in the future + +All deploy steps are driven by GitHub Actions: build on push to `develop` (and on release), then PRs to cluster repos. No manual image pushes or manual edits to cluster repos for routine deploys. + +- **Ping**: In the follow-up, note that future fixes are **merge to develop β†’ CI builds β†’ merge deploy PRs** (and for live: **create release β†’ merge live deploy PR**). diff --git a/docs/MIGRATION_PDF_AUTH.md b/docs/MIGRATION_PDF_AUTH.md index d5f7df26..a0bbad72 100644 --- a/docs/MIGRATION_PDF_AUTH.md +++ b/docs/MIGRATION_PDF_AUTH.md @@ -278,8 +278,7 @@ If issues occur: ## Environment Variables -No new environment variables required. Uses existing: -- `VITE_API_BASE_URL` - Frontend API base URL +No new environment variables required. Production uses relative API URLs (no env needed). Local dev may use `VITE_API_BASE_URL` in `frontend/.env` for the Vite proxy. ## Known Issues / Limitations diff --git a/frontend/.env b/frontend/.env index 2bfce617..b6cfc3de 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,2 +1,2 @@ -# VITE_API_BASE_URL=https://balancertestsite.com/ -VITE_API_BASE_URL=http://localhost:8000 \ No newline at end of file +# Optional: add VITE_* vars here if needed. None required for docker-compose; +# the app uses relative API URLs and vite.config.ts proxies /api to the backend. \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production deleted file mode 100644 index 71adcf10..00000000 --- a/frontend/.env.production +++ /dev/null @@ -1 +0,0 @@ -VITE_API_BASE_URL=https://balancerproject.org/ \ No newline at end of file diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 644708f8..84cebbb0 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -7,7 +7,7 @@ import { endpoints, } from "./endpoints"; -// Use empty string for relative URLs - all API calls will be relative to current domain +// Empty baseURL so API calls are relative to current origin; one image works for both sandbox and production. const baseURL = ""; export const publicApi = axios.create({ baseURL }); diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 9f917a94..bdc465ca 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -180,9 +180,10 @@ # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = "/static/" -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "build/static"), -] +STATICFILES_DIRS = [] +if os.path.exists(os.path.join(BASE_DIR, "build/static")): + STATICFILES_DIRS.append(os.path.join(BASE_DIR, "build/static")) + STATIC_ROOT = os.path.join(BASE_DIR, "static") AUTHENTICATION_BACKENDS = [ diff --git a/server/balancer_backend/urls.py b/server/balancer_backend/urls.py index d34c532f..958ef7c9 100644 --- a/server/balancer_backend/urls.py +++ b/server/balancer_backend/urls.py @@ -51,8 +51,21 @@ path("api/", include(api_urlpatterns)), ] -# Add a catch-all URL pattern for handling SPA (Single Page Application) routing -# Serve 'index.html' for any unmatched URL (must come after /api/ routes) +import os +from django.conf import settings +from django.http import HttpResponseNotFound + + +def spa_fallback(request): + """Serve index.html for SPA routing when build is present; otherwise 404.""" + index_path = os.path.join(settings.BASE_DIR, "build", "index.html") + if os.path.exists(index_path): + return TemplateView.as_view(template_name="index.html")(request) + return HttpResponseNotFound() + + +# Always register SPA catch-all so production serves the frontend regardless of +# URL config load order. At request time we serve index.html if build exists, else 404. urlpatterns += [ - re_path(r"^.*$", TemplateView.as_view(template_name="index.html")), + re_path(r"^(?!api|admin|static).*$", spa_fallback), ]