working dev environment
This commit is contained in:
+3
-2
@@ -31,6 +31,7 @@ See [README.md](README.md) for a full architecture map. Key files:
|
|||||||
- **Re-skin:** Edit colors in `_variables.scss` only.
|
- **Re-skin:** Edit colors in `_variables.scss` only.
|
||||||
- **Add a page:** Create component folder → add route in `app.routes.ts` → add nav link (if needed).
|
- **Add a page:** Create component folder → add route in `app.routes.ts` → add nav link (if needed).
|
||||||
- **Update content (dev):** Run the API, then use Swagger UI at [http://localhost:8000/docs](http://localhost:8000/docs) to create/update content.
|
- **Update content (dev):** Run the API, then use Swagger UI at [http://localhost:8000/docs](http://localhost:8000/docs) to create/update content.
|
||||||
- **Update content (seed):** Edit [backend/seed.py](backend/seed.py) and run `docker compose exec api python seed.py`.
|
- **Update content (seed):** Edit [backend/seed.py](backend/seed.py) and restart the API (autoseed runs on empty DB), or hit `curl -X POST http://localhost:8000/api/admin/reset` to re-seed.
|
||||||
- **Run the app:** `docker compose up --build` → [localhost:4200](http://localhost:4200).
|
- **Run the app (dev):** From the `backend/` directory, set `KMTN_DATABASE_URL=sqlite+aiosqlite:///./kmountain.db`, then `uvicorn app.main:app --reload` + `ng serve` in parallel. Autoseed populates data on first run.
|
||||||
|
- **Run the app (Docker):** `docker compose up --build` → [localhost:4200](http://localhost:4200).
|
||||||
- **Build:** `ng build` → `dist/`, then `docker compose up --build -d` for full stack.
|
- **Build:** `ng build` → `dist/`, then `docker compose up --build -d` for full stack.
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"*"
|
"Bash(*)",
|
||||||
|
"Read(*)",
|
||||||
|
"Edit(*)",
|
||||||
|
"Write(*)",
|
||||||
|
"Glob(*)",
|
||||||
|
"Grep(*)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"/workspaces/web_app/.vscode"
|
"/workspaces/web_app/.vscode"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
FROM ubuntu:latest
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
RUN apt update -y && apt install -y git npm python3 bash podman podman-compose fuse-overlayfs
|
RUN apk update && apk add git nodejs npm
|
||||||
|
|
||||||
# Install Angular.js via NPM
|
# Install Angular.js via NPM
|
||||||
RUN npm install -g @angular/cli@21.2.14
|
RUN npm install -g @angular/cli@21.2.14
|
||||||
|
|
||||||
# ENV SHELL /bin/bash
|
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"Angular.ng-template",
|
"Angular.ng-template",
|
||||||
"magnobiet.sass-extension-pack"
|
"magnobiet.sass-extension-pack",
|
||||||
|
"Anthropic.claude-code"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,3 +45,6 @@ Thumbs.db
|
|||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Dev SQLite database
|
||||||
|
backend/kmountain.db
|
||||||
@@ -6,27 +6,19 @@ Renders a hero page, about page, broadcast schedule, donation tiers, upcoming ev
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Option A — Docker Compose (recommended)
|
### Option A — Dev Container (recommended)
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
# Start all services
|
|
||||||
docker compose up --build
|
|
||||||
|
|
||||||
# Seed the database (first run only)
|
|
||||||
docker compose exec api python seed.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:4200](http://localhost:4200).
|
Open [http://localhost:4200](http://localhost:4200).
|
||||||
|
|
||||||
### Option B — Manual (frontend + backend separately)
|
### Option B — Manual (frontend + backend separately)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Database
|
# Backend (uses SQLite for local dev — no database server needed)
|
||||||
createdb kmountain
|
cd backend
|
||||||
export KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain
|
pip install -r requirements.txt
|
||||||
|
export KMTN_DATABASE_URL="sqlite+aiosqlite:///./kmountain.db"
|
||||||
# Backend
|
|
||||||
cd backend && pip install -r requirements.txt
|
|
||||||
uvicorn app.main:app --reload
|
uvicorn app.main:app --reload
|
||||||
|
|
||||||
# Frontend (new terminal)
|
# Frontend (new terminal)
|
||||||
@@ -39,11 +31,10 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru
|
|||||||
|
|
||||||
| Requirement | Version | Notes |
|
| Requirement | Version | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Docker + Docker Compose | 24+ | For the Docker Compose path |
|
| Node.js | 20+ | For frontend development |
|
||||||
| Node.js | 20+ | For manual frontend development |
|
|
||||||
| npm | 11+ | |
|
| npm | 11+ | |
|
||||||
| Python | 3.12+ | For manual backend development |
|
| Python | 3.12+ | For backend development |
|
||||||
| PostgreSQL | 16+ | For manual backend development (Docker handles it) |
|
| PostgreSQL | 16+ | For production only (dev uses SQLite) |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -58,29 +49,28 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru
|
|||||||
┌────▼─────┐ ┌──────▼─────┐
|
┌────▼─────┐ ┌──────▼─────┐
|
||||||
│ Nginx │ /api/* │ FastAPI │
|
│ Nginx │ /api/* │ FastAPI │
|
||||||
│ (static) │─ proxy ──▶ │ :8000 │
|
│ (static) │─ proxy ──▶ │ :8000 │
|
||||||
└────┬─────┘ └──────┬─────┘
|
└──────────┘ └──────┬─────┘
|
||||||
│ │
|
│
|
||||||
┌────▼─────┐ ┌──────▼─────┐
|
┌──────▼─────┐
|
||||||
│ Angular 21│ │ PostgreSQL │
|
│ PostgreSQL │
|
||||||
│ SPA │ │ :5432 │
|
│ :5432 │
|
||||||
└──────────┘ └────────────┘
|
└────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Three tiers:**
|
**Three tiers:**
|
||||||
|
|
||||||
1. **Frontend** — Angular 21 SPA built via multi-stage Dockerfile and served by Nginx. In production, Nginx serves the static files and proxies `/api/*` requests to the backend. In development, both run on separate ports and the browser talks directly to both.
|
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`.
|
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).
|
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
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── docker-compose.yml # 3-service stack (db, api, frontend)
|
├── docker-compose.dev.yml # Dev-sidecar compose (dev container)
|
||||||
├── Dockerfile # Multi-stage: Angular build → Nginx serve
|
|
||||||
├── nginx.conf # Static files + /api/* proxy
|
|
||||||
├── .env.example # Environment variable template
|
|
||||||
├── .devcontainer/ # VS Code dev container
|
├── .devcontainer/ # VS Code dev container
|
||||||
|
│ ├── Dockerfile # Alpine Python 3.12 + Node.js + Angular CLI
|
||||||
|
│ └── devcontainer.json # Dev container config
|
||||||
│
|
│
|
||||||
├── src/ # Angular 21 frontend
|
├── src/ # Angular 21 frontend
|
||||||
│ ├── app/ # Standalone components
|
│ ├── app/ # Standalone components
|
||||||
@@ -95,7 +85,6 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru
|
|||||||
│ └── _themes.scss # SCSS → CSS custom properties
|
│ └── _themes.scss # SCSS → CSS custom properties
|
||||||
│
|
│
|
||||||
└── backend/ # FastAPI backend
|
└── backend/ # FastAPI backend
|
||||||
├── Dockerfile # Python gunicorn/uvicorn
|
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── seed.py # Initial data for all models
|
├── seed.py # Initial data for all models
|
||||||
└── app/
|
└── app/
|
||||||
@@ -112,43 +101,40 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru
|
|||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
### Docker Compose
|
### Dev Container (recommended)
|
||||||
|
|
||||||
```bash
|
Open the repo in VS Code → **Reopen in Container**. The dev container starts both services automatically. See [.devcontainer/](.devcontainer/) for the configuration.
|
||||||
docker compose up --build # Start all services
|
|
||||||
docker compose exec api python seed.py # Seed database (first run)
|
|
||||||
docker compose logs -f api # Stream backend logs
|
|
||||||
docker compose down # Stop and remove containers
|
|
||||||
```
|
|
||||||
|
|
||||||
The `pgdata` volume persists the database across container restarts.
|
|
||||||
|
|
||||||
### Manual Development
|
### Manual Development
|
||||||
|
|
||||||
Run all three layers in parallel:
|
Run both layers in parallel — development uses SQLite (no database server needed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Database (install PostgreSQL 16 locally)
|
# 1. Backend (new terminal)
|
||||||
createdb kmountain
|
|
||||||
|
|
||||||
# 2. Backend (new terminal)
|
|
||||||
cd backend
|
cd backend
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
export KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain
|
export KMTN_DATABASE_URL="sqlite+aiosqlite:///./kmountain.db"
|
||||||
export KMTN_CORS_ORIGINS='["http://localhost:4200"]'
|
export KMTN_CORS_ORIGINS='["http://localhost:4200"]'
|
||||||
uvicorn app.main:app --reload
|
uvicorn app.main:app --reload
|
||||||
|
|
||||||
# 3. Frontend (another terminal)
|
# 2. Frontend (another terminal)
|
||||||
npm install
|
npm install
|
||||||
ng serve
|
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
|
## Configuration
|
||||||
|
|
||||||
| Variable | Default | Where | Description |
|
| Variable | Default | Where | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `KMTN_DATABASE_URL` | `postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain` | [backend/app/config.py](backend/app/config.py) | Async PostgreSQL connection string |
|
| `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_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 |
|
||||||
| `apiBaseUrl` | `http://localhost:8000` (dev) / `''` (prod) | [src/environments/](src/environments/) | Where the Angular app finds the API |
|
| `apiBaseUrl` | `http://localhost:8000` (dev) / `''` (prod) | [src/environments/](src/environments/) | Where the Angular app finds the API |
|
||||||
|
|
||||||
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.
|
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.
|
||||||
@@ -158,21 +144,17 @@ Environment variables are prefixed with `KMTN_` in the backend. For production,
|
|||||||
```bash
|
```bash
|
||||||
# Frontend (produces dist/radio-station/browser/)
|
# Frontend (produces dist/radio-station/browser/)
|
||||||
ng build --configuration production
|
ng build --configuration production
|
||||||
|
|
||||||
# Backend Docker image
|
|
||||||
docker build -t kmountain-api -f backend/Dockerfile backend/
|
|
||||||
|
|
||||||
# Full production stack
|
|
||||||
docker compose up --build -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For production deployment, serve the built static files with Nginx and run the backend behind it.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ng test # Angular + Vitest
|
ng test # Angular + Vitest
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend API is self-documented at [http://localhost:8000/docs](http://localhost:8000/docs) (Swagger UI). Run `docker compose exec api python seed.py` and visit that URL to interactively test endpoints.
|
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
|
## API Reference
|
||||||
|
|
||||||
@@ -200,33 +182,41 @@ Full interactive API docs at [http://localhost:8000/docs](http://localhost:8000/
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Production (Docker Compose on a VPS)
|
### 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
|
```bash
|
||||||
# 1. Clone repo on server
|
# 1. Build the frontend
|
||||||
git clone <repo-url> kmountain
|
ng build --configuration production
|
||||||
cd kmountain
|
|
||||||
|
|
||||||
# 2. Edit docker-compose.yml
|
# 2. Configure the backend
|
||||||
# - Change POSTGRES_PASSWORD to a strong value
|
export KMTN_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/kmountain"
|
||||||
# - Update KMTN_CORS_ORIGINS to your domain
|
export KMTN_CORS_ORIGINS='["https://yourdomain.com"]'
|
||||||
# - Optionally set KMTN_DATABASE_URL to a managed PostgreSQL
|
export KMTN_ENV="production"
|
||||||
|
|
||||||
# 3. Build and start
|
# 3. Start the backend
|
||||||
docker compose up --build -d
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
# 4. Seed the database
|
# 4. Serve the frontend with Nginx (or your preferred web server),
|
||||||
docker compose exec api python seed.py
|
# pointing /api/* requests to http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The database auto-seeds on first startup.
|
||||||
|
|
||||||
### Production hardening checklist
|
### Production hardening checklist
|
||||||
|
|
||||||
- [ ] Change the default `POSTGRES_PASSWORD` in [docker-compose.yml](docker-compose.yml)
|
- [ ] Use a managed PostgreSQL database (RDS, Cloud SQL, etc.)
|
||||||
- [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`)
|
- [ ] Update `KMTN_CORS_ORIGINS` to your actual domain (not `localhost`)
|
||||||
- [ ] Set `KMTN_DATABASE_URL` to a managed PostgreSQL connection (RDS, Cloud SQL, etc.)
|
- [ ] Set `KMTN_ENV="production"` to disable admin endpoints
|
||||||
- [ ] Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS
|
- [ ] Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS
|
||||||
- [ ] Set up log rotation for Docker containers
|
- [ ] Set up log rotation
|
||||||
- [ ] Configure Docker image registry and push/pull workflow
|
- [ ] Configure a process manager (systemd, PM2) for the backend
|
||||||
|
|
||||||
## Development Tools
|
## Development Tools
|
||||||
|
|
||||||
@@ -245,6 +235,6 @@ docker compose exec api python seed.py
|
|||||||
| Styling | SCSS with design tokens (variables → mixins → CSS custom properties) |
|
| Styling | SCSS with design tokens (variables → mixins → CSS custom properties) |
|
||||||
| Routing | Angular Router (lazy loaded) + Nginx SPA fallback |
|
| Routing | Angular Router (lazy loaded) + Nginx SPA fallback |
|
||||||
| HTTP | RxJS observables (frontend → FastAPI) |
|
| HTTP | RxJS observables (frontend → FastAPI) |
|
||||||
| Containers | Docker Compose (3 services: db, api, frontend) |
|
| Containers | Dev container (VS Code Remote - Containers) |
|
||||||
| CI/CD | _(not yet configured)_ |
|
| CI/CD | _(not yet configured)_ |
|
||||||
| Testing | Vitest (via Angular CLI) |
|
| Testing | Vitest (via Angular CLI) |
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.database import get_session
|
||||||
|
from app.models import DonationTier, Event, Program
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reset")
|
||||||
|
async def reset_database():
|
||||||
|
"""Truncate all tables and re-seed. Dev-only — disabled in production."""
|
||||||
|
if settings.ENV == "production":
|
||||||
|
return {"error": "Admin reset is disabled in production"}
|
||||||
|
|
||||||
|
from app.database import async_session
|
||||||
|
from seed import seed as run_seed
|
||||||
|
|
||||||
|
async with async_session() as session:
|
||||||
|
await session.execute(Program.__table__.delete())
|
||||||
|
await session.execute(Event.__table__.delete())
|
||||||
|
await session.execute(DonationTier.__table__.delete())
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await run_seed()
|
||||||
|
return {"status": "reset complete"}
|
||||||
@@ -4,6 +4,7 @@ from pydantic_settings import BaseSettings
|
|||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain"
|
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain"
|
||||||
CORS_ORIGINS: list[str] = ["http://localhost:4200", "http://localhost:3000"]
|
CORS_ORIGINS: list[str] = ["http://localhost:4200", "http://localhost:3000"]
|
||||||
|
ENV: str = "development"
|
||||||
|
|
||||||
model_config = {"env_prefix": "KMTN_"}
|
model_config = {"env_prefix": "KMTN_"}
|
||||||
|
|
||||||
|
|||||||
+19
-3
@@ -2,18 +2,29 @@ from contextlib import asynccontextmanager
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.database import engine
|
from app.database import async_session, engine
|
||||||
from app.models import Base
|
from app.models import Base, Program
|
||||||
from app.api import programs, events, tiers
|
from app.api import programs, events, tiers
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
# Create tables on startup (seed.py handles data seeding)
|
# Create tables on startup
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
# Auto-seed if database is empty
|
||||||
|
async with async_session() as session:
|
||||||
|
result = await session.execute(select(func.count(Program.id)))
|
||||||
|
count = result.scalar()
|
||||||
|
if count == 0:
|
||||||
|
print(" → Database is empty — running seed...")
|
||||||
|
from seed import seed as run_seed
|
||||||
|
await run_seed()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@@ -36,3 +47,8 @@ app.add_middleware(
|
|||||||
app.include_router(programs.router, prefix="/api/programs", tags=["programs"])
|
app.include_router(programs.router, prefix="/api/programs", tags=["programs"])
|
||||||
app.include_router(events.router, prefix="/api/events", tags=["events"])
|
app.include_router(events.router, prefix="/api/events", tags=["events"])
|
||||||
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
|
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
|
||||||
|
|
||||||
|
# Dev-only admin router (disabled in production)
|
||||||
|
if settings.ENV != "production":
|
||||||
|
from app.api import admin
|
||||||
|
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy import (
|
|||||||
Numeric,
|
Numeric,
|
||||||
Boolean,
|
Boolean,
|
||||||
Date,
|
Date,
|
||||||
ARRAY,
|
JSON,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
@@ -47,5 +47,5 @@ class DonationTier(Base):
|
|||||||
name = Column(String(50), unique=True, nullable=False)
|
name = Column(String(50), unique=True, nullable=False)
|
||||||
amount = Column(Numeric(10, 2), nullable=False)
|
amount = Column(Numeric(10, 2), nullable=False)
|
||||||
icon = Column(String(10), nullable=False)
|
icon = Column(String(10), nullable=False)
|
||||||
benefits = Column(ARRAY(Text), nullable=False)
|
benefits = Column(JSON, nullable=False)
|
||||||
display_order = Column(Integer, nullable=False)
|
display_order = Column(Integer, nullable=False)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ fastapi==0.115.6
|
|||||||
uvicorn[standard]==0.34.0
|
uvicorn[standard]==0.34.0
|
||||||
psycopg2-binary==2.9.10
|
psycopg2-binary==2.9.10
|
||||||
asyncpg==0.30.0
|
asyncpg==0.30.0
|
||||||
|
aiosqlite==0.20.0
|
||||||
sqlalchemy[asyncio]==2.0.36
|
sqlalchemy[asyncio]==2.0.36
|
||||||
alembic==1.14.1
|
alembic==1.14.1
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
|
|||||||
+68
-10
@@ -1,14 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
Seed script: populates the database with station data.
|
Seed script: populates the database with station data.
|
||||||
|
|
||||||
|
Idempotent — safe to run multiple times. Uses get-or-create (upsert) for each entity.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
DATABASE_URL=postgresql://... python -m app.seed
|
KMTN_DATABASE_URL=... python seed.py
|
||||||
(or from the repo root: cd backend && python seed.py)
|
(run from the backend/ directory)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.database import async_session
|
from app.database import async_session
|
||||||
from app.models import Program, Event, DonationTier
|
from app.models import Program, Event, DonationTier
|
||||||
|
|
||||||
@@ -90,29 +94,83 @@ TIERS: list[dict] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _upsert_program(session, data: dict) -> None:
|
||||||
|
"""Get-or-create a program matched on (day_of_week, time)."""
|
||||||
|
existing = await session.execute(
|
||||||
|
select(Program).where(
|
||||||
|
Program.day_of_week == data["day_of_week"],
|
||||||
|
Program.time == data["time"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
program = existing.scalar_one_or_none()
|
||||||
|
if program:
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(program, key, value)
|
||||||
|
else:
|
||||||
|
session.add(Program(**data))
|
||||||
|
|
||||||
|
|
||||||
|
async def _upsert_event(session, data: dict) -> None:
|
||||||
|
"""Get-or-create an event matched on (date, title)."""
|
||||||
|
existing = await session.execute(
|
||||||
|
select(Event).where(
|
||||||
|
Event.date == data["date"],
|
||||||
|
Event.title == data["title"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
event = existing.scalar_one_or_none()
|
||||||
|
if event:
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(event, key, value)
|
||||||
|
else:
|
||||||
|
session.add(Event(**data, active=True))
|
||||||
|
|
||||||
|
|
||||||
|
async def _upsert_tier(session, data: dict) -> None:
|
||||||
|
"""Get-or-create a donation tier matched on name."""
|
||||||
|
existing = await session.execute(
|
||||||
|
select(DonationTier).where(DonationTier.name == data["name"])
|
||||||
|
)
|
||||||
|
tier = existing.scalar_one_or_none()
|
||||||
|
if tier:
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(tier, key, value)
|
||||||
|
else:
|
||||||
|
session.add(DonationTier(**data))
|
||||||
|
|
||||||
|
|
||||||
# ── Main ──────────────────────────────────────────────────
|
# ── Main ──────────────────────────────────────────────────
|
||||||
|
|
||||||
async def seed():
|
async def seed() -> None:
|
||||||
|
"""Populate the database with station data (idempotent)."""
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
# Programs
|
|
||||||
for data in PROGRAMS:
|
for data in PROGRAMS:
|
||||||
session.add(Program(**data))
|
await _upsert_program(session, data)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
print(f" ✓ Seeded {len(PROGRAMS)} programs")
|
print(f" ✓ Seeded {len(PROGRAMS)} programs")
|
||||||
|
|
||||||
# Events
|
|
||||||
for data in EVENTS:
|
for data in EVENTS:
|
||||||
event = Event(**data, active=True)
|
await _upsert_event(session, data)
|
||||||
session.add(event)
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
print(f" ✓ Seeded {len(EVENTS)} events")
|
print(f" ✓ Seeded {len(EVENTS)} events")
|
||||||
|
|
||||||
# Tiers
|
|
||||||
for data in TIERS:
|
for data in TIERS:
|
||||||
session.add(DonationTier(**data))
|
await _upsert_tier(session, data)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
print(f" ✓ Seeded {len(TIERS)} donation tiers")
|
print(f" ✓ Seeded {len(TIERS)} donation tiers")
|
||||||
|
|
||||||
|
|
||||||
|
async def truncate_all() -> None:
|
||||||
|
"""Remove all seed data from the database."""
|
||||||
|
async with async_session() as session:
|
||||||
|
await session.execute(Program.__table__.delete())
|
||||||
|
await session.execute(Event.__table__.delete())
|
||||||
|
await session.execute(DonationTier.__table__.delete())
|
||||||
|
await session.commit()
|
||||||
|
print(" ✓ Truncated all tables")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(seed())
|
asyncio.run(seed())
|
||||||
|
|||||||
+3
-23
@@ -1,5 +1,6 @@
|
|||||||
# Dev-sidecar compose: app container + PostgreSQL sidecar.
|
# Dev-sidecar compose: app container only (no database service).
|
||||||
# Used by .devcontainer/devcontainer.json via dockerComposeFile.
|
# Used by .devcontainer/devcontainer.json via dockerComposeFile.
|
||||||
|
# Development uses SQLite — no Docker-in-Docker database needed.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -11,15 +12,12 @@ services:
|
|||||||
- "8000:8000" # FastAPI
|
- "8000:8000" # FastAPI
|
||||||
- "4200:4200" # Angular dev server
|
- "4200:4200" # Angular dev server
|
||||||
environment:
|
environment:
|
||||||
KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
|
KMTN_DATABASE_URL: sqlite+aiosqlite:///./backend/kmountain.db
|
||||||
KMTN_CORS_ORIGINS: '["http://localhost:4200"]'
|
KMTN_CORS_ORIGINS: '["http://localhost:4200"]'
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspaces/web_app:cached
|
- ..:/workspaces/web_app:cached
|
||||||
- node_modules:/workspaces/web_app/node_modules
|
- node_modules:/workspaces/web_app/node_modules
|
||||||
- backend_venv:/app/.venv
|
- backend_venv:/app/.venv
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
command: >
|
command: >
|
||||||
sh -c "
|
sh -c "
|
||||||
pip install -r backend/requirements.txt &&
|
pip install -r backend/requirements.txt &&
|
||||||
@@ -30,24 +28,6 @@ services:
|
|||||||
npx ng serve --host 0.0.0.0 --port 4200
|
npx ng serve --host 0.0.0.0 --port 4200
|
||||||
"
|
"
|
||||||
|
|
||||||
db:
|
|
||||||
image: docker.io/library/postgres:16-alpine
|
|
||||||
restart: "no"
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: kmountain
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
volumes:
|
|
||||||
- pgdata:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
|
||||||
node_modules:
|
node_modules:
|
||||||
backend_venv:
|
backend_venv:
|
||||||
|
|||||||
Generated
-90
@@ -2204,9 +2204,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2224,9 +2221,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2244,9 +2238,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2264,9 +2255,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2284,9 +2272,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2304,9 +2289,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2324,9 +2306,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2770,9 +2749,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2794,9 +2770,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2818,9 +2791,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2842,9 +2812,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2866,9 +2833,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2890,9 +2854,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3070,9 +3031,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3090,9 +3048,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3110,9 +3065,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3130,9 +3082,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3309,9 +3258,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3326,9 +3272,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3343,9 +3286,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3360,9 +3300,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3377,9 +3314,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3394,9 +3328,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3411,9 +3342,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3428,9 +3356,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3445,9 +3370,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3462,9 +3384,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3479,9 +3398,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3496,9 +3412,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3513,9 +3426,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
Reference in New Issue
Block a user