375 lines
14 KiB
Markdown
375 lines
14 KiB
Markdown
# 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 <registry>/kmtnflower:latest .
|
|
docker push <registry>/kmtnflower:latest
|
|
|
|
# Backend
|
|
docker build -t <registry>/kmtnflower-api:latest ./backend
|
|
docker push <registry>/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: <registry>/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: <registry>/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) |
|