# KMountain Flower Radio Station A three-tier web application for a community radio station: Angular 21 frontend, FastAPI backend, PostgreSQL database. Renders a hero page, about page, broadcast schedule, donation tiers, upcoming events, and a contact form — styled with a warm mountain/nature aesthetic. ## Quick Start ### Option A — Dev Container (recommended) Open the repo in VS Code and click **Reopen in Container** (or run `Dev Containers: Reopen in Container` from the command palette). The dev container starts both the backend (port 8000) and frontend (port 4200) automatically. Open [http://localhost:4200](http://localhost:4200). ### Option B — Manual (frontend + backend separately) ```bash # Backend (uses SQLite for local dev — no database server needed) cd backend pip install -r requirements.txt export KMTN_DATABASE_URL="sqlite+aiosqlite:///./kmountain.db" uvicorn app.main:app --reload # Frontend (new terminal) npm install && ng serve ``` Open [http://localhost:4200](http://localhost:4200) and ensure the backend is running on [http://localhost:8000](http://localhost:8000). ## Prerequisites | Requirement | Version | Notes | |---|---|---| | Node.js | 20+ | For frontend development | | npm | 11+ | | | Python | 3.12+ | For backend development | | PostgreSQL | 16+ | For production only (dev uses SQLite) | ## Architecture ``` ┌──────────────┐ │ Browser │ │ :4200 / :80│ └──────┬───────┘ │ ┌────────────┴────────────┐ │ │ ┌────▼─────┐ ┌──────▼─────┐ │ Nginx │ /api/* │ FastAPI │ │ (static) │─ proxy ──▶ │ :8000 │ └──────────┘ └──────┬─────┘ │ ┌──────▼─────┐ │ PostgreSQL │ │ :5432 │ └────────────┘ ``` **Three tiers:** 1. **Frontend** — Angular 21 SPA. In production, served by Nginx. In development, served by the Angular dev server on port 4200. 2. **Backend** — Python FastAPI with async SQLAlchemy. Provides CRUD REST endpoints for programs (broadcast schedule), events (upcoming events), and donation tiers. Auto-generates Swagger/OpenAPI docs at `/docs`. 3. **Database** — PostgreSQL 16. Schema contains three tables: `programs`, `events`, `donation_tiers`. Initial data is loaded via [seed.py](backend/seed.py). Local development uses SQLite for convenience. ## Project Structure ``` . ├── docker-compose.yml # Dev compose (builds from source) ├── docker-compose.prod.yml # Prod override (applied with -f docker-compose.yml -f docker-compose.prod.yml) ├── docker-compose.dev.yml # Dev-sidecar compose (dev container) ├── Dockerfile # Frontend: Node build stage + Nginx serve stage ├── docker-entrypoint.sh # Renders nginx config + config.json from env vars ├── nginx.conf.template # Nginx config template (envsubst) ├── config.json.template # Runtime API config template (envsubst) ├── .devcontainer/ # VS Code dev container │ ├── Dockerfile # Alpine Python 3.12 + Node.js + Angular CLI │ └── devcontainer.json # Dev container config │ ├── src/ # Angular 21 frontend │ ├── app/ # Standalone components │ │ ├── app.routes.ts # Lazy-loaded route map │ │ ├── app.config.ts # App providers + APP_INITIALIZER (loads config.json) │ │ ├── hero/ about/ schedule/ donate/ events/ contact/ navbar/ footer/ │ │ ├── services/ # HTTP services (program, event, tier, app-config) │ │ └── interfaces/ # TypeScript types (Program, Event, Tier) │ ├── environments/ # dev/prod config (apiBaseUrl) │ └── styles/ # SCSS design tokens │ ├── _variables.scss # Colors, spacing, breakpoints │ ├── _mixins.scss # Card, button, responsive mixins │ └── _themes.scss # SCSS → CSS custom properties │ └── backend/ # FastAPI backend ├── Dockerfile # Python 3.12-slim + gunicorn/uvicorn ├── requirements.txt ├── seed.py # Initial data for all models └── app/ ├── main.py # FastAPI app, CORS, router registration ├── config.py # ENV-based settings (KMTN_* prefix) ├── database.py # async engine + session factory ├── models.py # SQLAlchemy ORM (Program, Event, DonationTier) ├── schemas.py # Pydantic schemas (Create/Update/Response) └── api/ ├── programs.py # CRUD /api/programs ├── events.py # CRUD /api/events └── tiers.py # CRUD /api/tiers ``` ## Local Development ### Dev Container (recommended) Open the repo in VS Code → **Reopen in Container**. The dev container starts both services automatically. See [.devcontainer/](.devcontainer/) for the configuration. ### Manual Development Run both layers in parallel — development uses SQLite (no database server needed): ```bash # 1. Backend (new terminal) cd backend pip install -r requirements.txt export KMTN_DATABASE_URL="sqlite+aiosqlite:///./kmountain.db" export KMTN_CORS_ORIGINS='["http://localhost:4200"]' uvicorn app.main:app --reload # 2. Frontend (another terminal) npm install ng serve ``` The database auto-seeds on first startup. To reset and re-seed at any time: ```bash curl -X POST http://localhost:8000/api/admin/reset ``` ## Configuration | Variable | Default | Where | Description | |---|---|---|---| | Variable | Default | Where | Description | |---|---|---|---| | `KMTN_DATABASE_URL` | `postgresql+asyncpg://...` (prod) / `sqlite+aiosqlite:///./kmountain.db` (dev) | [backend/app/config.py](backend/app/config.py) | Database connection string | | `KMTN_CORS_ORIGINS` | `["http://localhost:4200"]` | [backend/app/config.py](backend/app/config.py) | CORS allowlist (JSON array) | | `KMTN_ENV` | `development` | [backend/app/config.py](backend/app/config.py) | Set to `production` to disable admin endpoints | | `API_UPSTREAM` | `http://api:8000` | [Dockerfile](Dockerfile) | Internal API target for Nginx proxy (Docker DNS) | | `API_PUBLIC_URL` | `""` (empty) | [Dockerfile](Dockerfile) | Browser-facing API URL. Empty = proxy mode; set to direct URL when ingress breaks proxy chain | Environment variables are prefixed with `KMTN_` in the backend. For production, set `KMTN_DATABASE_URL` to your managed PostgreSQL connection and update `KMTN_CORS_ORIGINS` to your actual domain. ## Building ```bash # Frontend (produces dist/radio-station/browser/) ng build --configuration production ``` For production deployment, serve the built static files with Nginx and run the backend behind it. ## Testing ```bash ng test # Angular + Vitest ``` The backend API is self-documented at [http://localhost:8000/docs](http://localhost:8000/docs) (Swagger UI). The database auto-seeds on first startup — visit that URL to interactively test endpoints. ## API Reference The backend exposes CRUD endpoints for three models: | Method | Endpoint | Description | |---|---|---| | `GET` | `/api/programs` | List all programs (`?day=1..7` to filter by day of week) | | `GET` | `/api/programs/:id` | Single program | | `POST` | `/api/programs` | Create program | | `PUT` | `/api/programs/:id` | Update program | | `DELETE` | `/api/programs/:id` | Delete program | | `GET` | `/api/events` | List events (`?active=true` to filter active events) | | `GET` | `/api/events/:id` | Single event | | `POST` | `/api/events` | Create event | | `PUT` | `/api/events/:id` | Update event | | `DELETE` | `/api/events/:id` | Delete event | | `GET` | `/api/tiers` | List donation tiers (ordered by `display_order`) | | `GET` | `/api/tiers/:id` | Single tier | | `POST` | `/api/tiers` | Create tier | | `PUT` | `/api/tiers/:id` | Update tier | | `DELETE` | `/api/tiers/:id` | Delete tier | Full interactive API docs at [http://localhost:8000/docs](http://localhost:8000/docs). ## Deployment ### Docker Compose (recommended) The app ships as two Docker images — a frontend (Angular + Nginx) and a backend (FastAPI + gunicorn). Build, tag, and push them to your registry, then deploy with Docker Compose. #### Build and push ```bash # Frontend docker build -t /kmtnflower:latest . docker push /kmtnflower:latest # Backend docker build -t /kmtnflower-api:latest ./backend docker push /kmtnflower-api:latest ``` #### Deploy On the target machine, create a Docker Compose file (see [docker-compose.test.yml](docker-compose.test.yml) for a reference) that pulls the images and sets the required environment variables: ```yaml services: db: image: docker.io/library/postgres:16-alpine environment: POSTGRES_DB: kmountain POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 api: image: /kmtnflower-api:latest environment: KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain KMTN_CORS_ORIGINS: '["http://yourdomain.com"]' KMTN_ENV: production depends_on: db: condition: service_healthy frontend: image: /kmtnflower:latest environment: API_UPSTREAM: http://api:8000 API_PUBLIC_URL: "" ports: - "4200:80" depends_on: - api volumes: pgdata: ``` Then start: ```bash docker compose up -d ``` #### API\_PUBLIC\_URL The frontend container renders a `config.json` at startup from the `API_PUBLIC_URL` environment variable. This controls how the Angular app reaches the API: | Mode | Value | When to use | |---|---|---| | **Proxy** (default) | `""` (empty) | Browser calls `/api/*` relative paths; Nginx forwards to `API_UPSTREAM`. Use when no external ingress interferes. | | **Direct** | `"http://yourhost:8000"` | Browser calls the API container directly. Use when an external ingress layer (e.g., TrueNAS middleware, Kubernetes ingress) breaks the Nginx proxy chain. Requires CORS to be configured on the backend. | #### Example: TrueNAS Scale For a TrueNAS Scale deployment where the built-in ingress layer may redirect traffic: ```yaml services: db: image: docker.io/library/postgres:16-alpine environment: POSTGRES_DB: kmountain POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 api: image: truenas.local:35000/kmtnflower-api:latest environment: KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain KMTN_CORS_ORIGINS: '["http://truenas.local:34200", "http://truenas.local"]' KMTN_ENV: production ports: - "8000:8000" depends_on: db: condition: service_healthy frontend: image: truenas.local:35000/kmtnflower:latest environment: API_UPSTREAM: http://api:8000 API_PUBLIC_URL: "http://truenas.local:8000" ports: - "34200:80" depends_on: - api volumes: pgdata: ``` > **Note:** The database auto-seeds with initial data on first startup. To re-seed at any time (dev only): `curl -X POST http://localhost:8000/api/admin/reset`. ### Bare-metal production For production, you'll need to set up: - A PostgreSQL database (managed or self-hosted) - The FastAPI backend (gunicorn/uvicorn) - Nginx to serve the Angular build and proxy `/api/*` to the backend ```bash # 1. Build the frontend ng build --configuration production # 2. Configure the backend export KMTN_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/kmountain" export KMTN_CORS_ORIGINS='["https://yourdomain.com"]' export KMTN_ENV="production" # 3. Start the backend cd backend pip install -r requirements.txt uvicorn app.main:app --host 0.0.0.0 --port 8000 # 4. Serve the frontend with Nginx (or your preferred web server), # pointing /api/* requests to http://localhost:8000 ``` The database auto-seeds on first startup. ### Production hardening checklist - [ ] Use a managed PostgreSQL database (RDS, Cloud SQL, etc.) - [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`) - [ ] Set `KMTN_ENV="production"` to disable admin endpoints - [ ] Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS - [ ] Set up log rotation - [ ] Configure a process manager (systemd, PM2) for the backend ## Development Tools - **Dev container** — Open the repo in VS Code → "Reopen in Container" for a pre-configured environment with Angular CLI, Node.js, Python 3, and Git ([.devcontainer/](.devcontainer/)). - **VS Code workspace** — [workspace file](web_app.code-workspace) in the repo root configures both Angular and Python extensions. - **Prettier** — Run `npx prettier --write .` to format the codebase. - **Prettier config** — [**.prettierrc**](.prettierrc) (100 char width, single quotes, Angular HTML parser). ## Tech Stack | Layer | Technology | |---|---| | Frontend | Angular 21 (standalone components, signals-ready) | | Backend | FastAPI 0.115, Python 3.12 | | Database | PostgreSQL 16 (async via SQLAlchemy 2.0) | | Styling | SCSS with design tokens (variables → mixins → CSS custom properties) | | Routing | Angular Router (lazy loaded) + Nginx SPA fallback | | HTTP | RxJS observables (frontend → FastAPI) | | Containers | Dev container (VS Code Remote - Containers) | | CI/CD | _(not yet configured)_ | | Testing | Vitest (via Angular CLI) |