From 57b882128d0e1e8b2d36437a5d012ff4a3790c14 Mon Sep 17 00:00:00 2001 From: kfj001 Date: Thu, 18 Jun 2026 08:29:03 +0000 Subject: [PATCH] working dev environment --- .claude/CLAUDE.md | 5 +- .claude/settings.local.json | 9 +- .devcontainer/Dockerfile | 8 +- .devcontainer/devcontainer.json | 3 +- .gitignore | 3 + README.md | 134 ++++++++---------- backend/__pycache__/seed.cpython-312.pyc | Bin 4886 -> 8567 bytes .../app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 140 bytes .../app/__pycache__/config.cpython-312.pyc | Bin 672 -> 747 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 1835 -> 2942 bytes .../app/__pycache__/models.cpython-312.pyc | Bin 2343 -> 2349 bytes .../api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 144 bytes .../app/api/__pycache__/admin.cpython-312.pyc | Bin 0 -> 1855 bytes backend/app/api/admin.py | 26 ++++ backend/app/config.py | 1 + backend/app/main.py | 22 ++- backend/app/models.py | 4 +- backend/requirements.txt | 1 + backend/seed.py | 78 ++++++++-- docker-compose.dev.yml | 26 +--- package-lock.json | 90 ------------ 21 files changed, 200 insertions(+), 210 deletions(-) create mode 100644 backend/app/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/admin.cpython-312.pyc create mode 100644 backend/app/api/admin.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0dcf2f0..da08252 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -31,6 +31,7 @@ See [README.md](README.md) for a full architecture map. Key files: - **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). - **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`. -- **Run the app:** `docker compose up --build` → [localhost:4200](http://localhost:4200). +- **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 (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. diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3deed1b..78cebb2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,10 +1,15 @@ { "permissions": { "allow": [ - "*" + "Bash(*)", + "Read(*)", + "Edit(*)", + "Write(*)", + "Glob(*)", + "Grep(*)" ], "additionalDirectories": [ "/workspaces/web_app/.vscode" ] } -} \ No newline at end of file +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 43fbde7..8fbf7cf 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 -RUN npm install -g @angular/cli@21.2.14 - -# ENV SHELL /bin/bash \ No newline at end of file +RUN npm install -g @angular/cli@21.2.14 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7a9e218..28b2c57 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,8 @@ "vscode": { "extensions": [ "Angular.ng-template", - "magnobiet.sass-extension-pack" + "magnobiet.sass-extension-pack", + "Anthropic.claude-code" ] } } diff --git a/.gitignore b/.gitignore index 882b9ac..68a7c15 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ Thumbs.db # Environment files .env + +# Dev SQLite database +backend/kmountain.db \ No newline at end of file diff --git a/README.md b/README.md index b28ff77..7561704 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,19 @@ Renders a hero page, about page, broadcast schedule, donation tiers, upcoming ev ## Quick Start -### Option A — Docker Compose (recommended) +### Option A — Dev Container (recommended) -```bash -# Start all services -docker compose up --build - -# Seed the database (first run only) -docker compose exec api python seed.py -``` +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 -# Database -createdb kmountain -export KMTN_DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain - -# Backend -cd backend && pip install -r requirements.txt +# 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) @@ -39,11 +31,10 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru | Requirement | Version | Notes | |---|---|---| -| Docker + Docker Compose | 24+ | For the Docker Compose path | -| Node.js | 20+ | For manual frontend development | +| Node.js | 20+ | For frontend development | | npm | 11+ | | -| Python | 3.12+ | For manual backend development | -| PostgreSQL | 16+ | For manual backend development (Docker handles it) | +| Python | 3.12+ | For backend development | +| PostgreSQL | 16+ | For production only (dev uses SQLite) | ## Architecture @@ -58,29 +49,28 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru ┌────▼─────┐ ┌──────▼─────┐ │ Nginx │ /api/* │ FastAPI │ │ (static) │─ proxy ──▶ │ :8000 │ - └────┬─────┘ └──────┬─────┘ - │ │ - ┌────▼─────┐ ┌──────▼─────┐ - │ Angular 21│ │ PostgreSQL │ - │ SPA │ │ :5432 │ - └──────────┘ └────────────┘ + └──────────┘ └──────┬─────┘ + │ + ┌──────▼─────┐ + │ PostgreSQL │ + │ :5432 │ + └────────────┘ ``` **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`. -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 ``` . -├── docker-compose.yml # 3-service stack (db, api, frontend) -├── Dockerfile # Multi-stage: Angular build → Nginx serve -├── nginx.conf # Static files + /api/* proxy -├── .env.example # Environment variable template +├── docker-compose.dev.yml # Dev-sidecar compose (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 │ ├── 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 │ └── backend/ # FastAPI backend - ├── Dockerfile # Python gunicorn/uvicorn ├── requirements.txt ├── seed.py # Initial data for all models └── app/ @@ -112,43 +101,40 @@ Open [http://localhost:4200](http://localhost:4200) and ensure the backend is ru ## Local Development -### Docker Compose +### Dev Container (recommended) -```bash -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. +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 all three layers in parallel: +Run both layers in parallel — development uses SQLite (no database server needed): ```bash -# 1. Database (install PostgreSQL 16 locally) -createdb kmountain - -# 2. Backend (new terminal) +# 1. Backend (new terminal) cd backend 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"]' uvicorn app.main:app --reload -# 3. Frontend (another terminal) +# 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 | |---|---|---|---| -| `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_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 | 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 # Frontend (produces dist/radio-station/browser/) 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 ```bash 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 @@ -200,33 +182,41 @@ Full interactive API docs at [http://localhost:8000/docs](http://localhost:8000/ ## 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 -# 1. Clone repo on server -git clone kmountain -cd kmountain +# 1. Build the frontend +ng build --configuration production -# 2. Edit docker-compose.yml -# - Change POSTGRES_PASSWORD to a strong value -# - Update KMTN_CORS_ORIGINS to your domain -# - Optionally set KMTN_DATABASE_URL to a managed PostgreSQL +# 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. Build and start -docker compose up --build -d +# 3. Start the backend +cd backend +pip install -r requirements.txt +uvicorn app.main:app --host 0.0.0.0 --port 8000 -# 4. Seed the database -docker compose exec api python seed.py +# 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 -- [ ] 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`) -- [ ] 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 -- [ ] Set up log rotation for Docker containers -- [ ] Configure Docker image registry and push/pull workflow +- [ ] Set up log rotation +- [ ] Configure a process manager (systemd, PM2) for the backend ## Development Tools @@ -245,6 +235,6 @@ docker compose exec api python seed.py | Styling | SCSS with design tokens (variables → mixins → CSS custom properties) | | Routing | Angular Router (lazy loaded) + Nginx SPA fallback | | 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)_ | | Testing | Vitest (via Angular CLI) | diff --git a/backend/__pycache__/seed.cpython-312.pyc b/backend/__pycache__/seed.cpython-312.pyc index db7128d476fe49bbf559f66b4f76c2bcdf95eae3..af1e52f09d1280d1f53090c39e573246eedf8701 100644 GIT binary patch literal 8567 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 delta 1891 zcmbtUU2GIp6ux(6cDDP|-Ii|4ZnrH%DQy?%RzN6iDcHITYAYyzN)^|kXDkcdA7=)z zwPe5=ElFc76A`q=YJAW%CdT??Vhp5x(I+rX$OaSj#h>`Ffd|m|;JMSz0BvH8lq6+$hA-ht3Yz()ld&>puv%iwa^Gn(Ch#) zSO@E2gCn;<+&*B?+wWC;ia&z4P)eb7JMJ7&cz6gl!lqfC-Hs603=hK=*a~gau4U~u zXm_+7&PMg(df-u8joJhx9qlgI4UfU&Ogkv_2kuMe36>1>LLWRi zNMO&$6sxy)+67O+)36nheTeM0kOOcK4k7XkBF{pO86Adxcn+cG5jw(*FnI93;4fIg zQ?HZ$GNl||Jkd;u8pgZP;|cF|^2itF6xVJX)Z{^>!zRS9_naqjmP6sepo-2%StU7NzUivnxcT^)SOeLm(*NF z(vK;Us^oK$n#<`Ok~9F)QF-8mk_D-;(e`F;@%(Vi`#Vc!PmGEM0c2g7o8tHKv~=!g z@f(gt=RWj&NmmXmCvI&Xq+_^le`S6bkti|04n;jRgq)$Hw3W&kjwMRW>%J9b+`S+Z4NG~g`_Fj=%=fCKDt(206dwLy9V%^& zoy7mEgL9CbG}b}9zYc&E1v0x_uQ6`9zMNg(VbVt4rV~wVWa3X;leo6E`N%|I4cSPT zx%q80YLHj>!{jvmoIg!tC3{Y?K1k9p)26f^M@9i@#3SX^+@LCFw1OxP=;>3+ff2W) zoKmv7HWHK|mzDK24yc}1RP8eo^Qi*HP^*v>co{%-F-{!FWin~K;L5A%tX|-Ul&p%q zBZew6>o_B9kYaqnttpy@5v>W83$CT3*vo?d%HOE9M!6s`M(sd2g-Z+IZ}uCxdzRb` z)=U}`#!S<%p_=i;4S&UC@A=+o_vPSB!{y+jzq!bh%G#d-m6O@?*&hPt=5S5x9g$Q< z#}hY0k!zv&r38{sL)Sy`@#M1iCD)?AuINIeqDX>O=1{mk!hEXl#rIzeZ$Vr>wM&f5Inp4&E9jp SQ-Ucj3M;XnLbbXJ!~6@05WV#P diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..951db82befdc65341ca968b0a8f01b5af6e9a4ef GIT binary patch literal 140 zcmX@j%ge<81i$08vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!($+7}FUl@1NK8&G z)-O*@icc&k&`(NC&Q8rs(Fd{P<1_OzOXB183My}L*yQG?l;)(`6|n;KG6Hcii1Cq` Lk&&^88OQlj0K#3iOi_le1IvQuHU^ zWRzhvnf#4WS?mVCOoMZW@MRX62O2_mBOCM7{yV^p~*S%_B2th)V#9zf}+&4 z%!*sA-o7Dz@qU_|lV>n`iQnSM%}+_qiBHbYOUq1O$?zFu)Gyx2QcN<8T9eJ0lzASA zC@fIFAgtBsQzSIGfXUcH5U8R^2t)`2iCY{tKod%HQtgVwfLul(E|vunAD9^#8SgS^ bePLr^)b6PH!T_W`@-gs;bXzrYfVBev;Bqm* diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc index a1c49722ab030be0f228818e3bf7e0d154954aa6..f4ac95db3f713c78fb004e3539914f0de3923ca2 100644 GIT binary patch literal 2942 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 D&tI`= diff --git a/backend/app/__pycache__/models.cpython-312.pyc b/backend/app/__pycache__/models.cpython-312.pyc index 8cdabdfe1e145d1bc6b16fcbcd5b844c57318f56..e304f657878b5029fa42579373a6653bb5c12e24 100644 GIT binary patch delta 141 zcmZ23v{s1sG%qg~0}$}_7-!v@$or0!#Vgp~Z!#lefvl!}d45rLaY15oYO#KKYEpb+ zL4kf!VsdtBUW)$aBF4KcjGHD0u=T5?@}#h?;atrGGKYa7ikFEYl_ixGMT~DUKf50z g|KvD!*U2l`%^4FXKVbJ~pOCU3^a{WBWGjxh05sMo?EnA( delta 124 zcmZ20v|NbyG%qg~0}z-dXlFf|$or0s)iEf@F>(@P0VD6`UdFpDjQb|bvuIDA#+Jgy z#E{C9$~*Z1o2V!skj;|Hn!>tFby07iAY0Bqpa8 z>zAh{#U~aN=qDv6XQ$?+=!4ja1)2Ks@tJv)vzDo%yM?HH2XNu|1v};1T+rF^vJdKTek6br%`PzIG(R64UXdtJGScdSO*e zPy)7@CxRmhm`$LQ9EZ@`2*Y8Wrkt%kJq)}!gb)=B&fv57U1SJGfTd#{Yypdq4q+YA z#G$q3VJ6TfHlz}RoxgJz&T#`Z64prKCaOo` zYmqQ|Mx^$JhvyzC_FN+UPb)QY@!uR65_@**_1xtCT@All%|>ln*J}lJm5m^)brNm= z0<;UX3^WUr0D8F(jUnHs02TK0o(Cb%ZI3PU*U=IN?f7^65{`#Tvyhc* zv}9&HL^BSG1RQ77O7>FGOX1{ zz0f)11qh`UUG1MS3ZUipyPCD1VM>jGVI36j0>jI{_)ajPc#T~LfOru zA`^<>xR7rEVqVWI64OX9 ztQWHhogIC&xZ>5eoo;Tf3%syTp8W`>N9c3|{UstfdVB5G+NWzjOMQpYPWQz(-L@H5 zo_Qz?9|`>r+In`%+vRTt4qD)pIQ&oA&m8TjCQaaWzZD{kJ~_gdMHl;C4r`al+$Bob-GjDDQ}g}j>9N$ z@yz}L4*Ow38^_;cEh_%N1Nw1H>*4PWV69!eH#h`9o8d5rN9dg&qRJwmVkipCGn_=8~k jo$d|sSVWwByZcu6C+}Bq$78My51sIc>+})r1>=7Jo1Cf@ literal 0 HcmV?d00001 diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py new file mode 100644 index 0000000..1d2c659 --- /dev/null +++ b/backend/app/api/admin.py @@ -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"} diff --git a/backend/app/config.py b/backend/app/config.py index 91bf5f5..85c2f98 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -4,6 +4,7 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/kmountain" CORS_ORIGINS: list[str] = ["http://localhost:4200", "http://localhost:3000"] + ENV: str = "development" model_config = {"env_prefix": "KMTN_"} diff --git a/backend/app/main.py b/backend/app/main.py index 9e0b8f9..b227cf4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,18 +2,29 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import func, select from app.config import settings -from app.database import engine -from app.models import Base +from app.database import async_session, engine +from app.models import Base, Program from app.api import programs, events, tiers @asynccontextmanager async def lifespan(app: FastAPI): - # Create tables on startup (seed.py handles data seeding) + # Create tables on startup async with engine.begin() as conn: 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 @@ -36,3 +47,8 @@ app.add_middleware( app.include_router(programs.router, prefix="/api/programs", tags=["programs"]) app.include_router(events.router, prefix="/api/events", tags=["events"]) 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"]) diff --git a/backend/app/models.py b/backend/app/models.py index 48af1e3..6defaa3 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -6,7 +6,7 @@ from sqlalchemy import ( Numeric, Boolean, Date, - ARRAY, + JSON, ) from sqlalchemy.orm import DeclarativeBase @@ -47,5 +47,5 @@ class DonationTier(Base): name = Column(String(50), unique=True, nullable=False) amount = Column(Numeric(10, 2), 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) diff --git a/backend/requirements.txt b/backend/requirements.txt index 69a6773..8cd3a41 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,6 +2,7 @@ fastapi==0.115.6 uvicorn[standard]==0.34.0 psycopg2-binary==2.9.10 asyncpg==0.30.0 +aiosqlite==0.20.0 sqlalchemy[asyncio]==2.0.36 alembic==1.14.1 python-multipart==0.0.20 diff --git a/backend/seed.py b/backend/seed.py index 4204ec2..9c094ce 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -1,14 +1,18 @@ """ Seed script: populates the database with station data. +Idempotent — safe to run multiple times. Uses get-or-create (upsert) for each entity. + Usage: - DATABASE_URL=postgresql://... python -m app.seed - (or from the repo root: cd backend && python seed.py) + KMTN_DATABASE_URL=... python seed.py + (run from the backend/ directory) """ import asyncio from datetime import date +from sqlalchemy import select + from app.database import async_session 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 ────────────────────────────────────────────────── -async def seed(): +async def seed() -> None: + """Populate the database with station data (idempotent).""" async with async_session() as session: - # Programs for data in PROGRAMS: - session.add(Program(**data)) + await _upsert_program(session, data) await session.commit() print(f" ✓ Seeded {len(PROGRAMS)} programs") - # Events for data in EVENTS: - event = Event(**data, active=True) - session.add(event) + await _upsert_event(session, data) await session.commit() print(f" ✓ Seeded {len(EVENTS)} events") - # Tiers for data in TIERS: - session.add(DonationTier(**data)) + await _upsert_tier(session, data) await session.commit() 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__": asyncio.run(seed()) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3d6a5f7..69125f1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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. +# Development uses SQLite — no Docker-in-Docker database needed. services: app: @@ -11,15 +12,12 @@ services: - "8000:8000" # FastAPI - "4200:4200" # Angular dev server 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"]' volumes: - ..:/workspaces/web_app:cached - node_modules:/workspaces/web_app/node_modules - backend_venv:/app/.venv - depends_on: - db: - condition: service_healthy command: > sh -c " pip install -r backend/requirements.txt && @@ -30,24 +28,6 @@ services: 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: - pgdata: node_modules: backend_venv: diff --git a/package-lock.json b/package-lock.json index a07c724..f959b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2204,9 +2204,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2224,9 +2221,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2244,9 +2238,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2264,9 +2255,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2284,9 +2272,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2304,9 +2289,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2324,9 +2306,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2770,9 +2749,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2794,9 +2770,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2818,9 +2791,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2842,9 +2812,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2866,9 +2833,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2890,9 +2854,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3070,9 +3031,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3090,9 +3048,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3110,9 +3065,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3130,9 +3082,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3309,9 +3258,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3326,9 +3272,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3343,9 +3286,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3360,9 +3300,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3377,9 +3314,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3394,9 +3328,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3411,9 +3342,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3428,9 +3356,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3445,9 +3370,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3462,9 +3384,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3479,9 +3398,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3496,9 +3412,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3513,9 +3426,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [