# RFCP Backend - Iteration 1.1: Foundation **Date:** January 30, 2025 **Type:** Backend Development **Estimated:** 2-3 hours **Location:** `/opt/rfcp/backend/` --- ## 🎯 Goal Set up basic FastAPI structure with MongoDB connection and CRUD endpoints for projects/sites. --- ## 📋 Pre-reading (IMPORTANT) Before starting, read these documents in project knowledge: 1. `RFCP-Backend-Roadmap-Complete.md` — full backend architecture 2. `RFCP-ARCHITECTURE.md` — system overview 3. `SESSION-2025-01-30-Complete.md` — context --- ## 📊 Current State ```bash # Service already running systemctl status rfcp-backend.service # ✅ active # Current structure /opt/rfcp/backend/ ├── app/ │ ├── __init__.py │ └── main.py # Basic FastAPI app ├── venv/ # Python 3.12, FastAPI installed ├── Dockerfile └── requirements.txt # MongoDB available (Open5GS instance) # Will use separate database: "rfcp" ``` --- ## ✅ Tasks ### 1. Install Dependencies ```bash cd /opt/rfcp/backend source venv/bin/activate pip install motor pydantic-settings pymongo pip install numpy scipy requests ``` Update `requirements.txt` with new deps. --- ### 2. Create Directory Structure ``` app/ ├── __init__.py ├── main.py ├── api/ │ ├── __init__.py │ ├── deps.py │ └── routes/ │ ├── __init__.py │ ├── projects.py │ └── health.py ├── core/ │ ├── __init__.py │ ├── config.py │ └── database.py └── models/ ├── __init__.py ├── project.py └── site.py ``` --- ### 3. Implement Core Components **app/core/config.py:** ```python from pydantic_settings import BaseSettings class Settings(BaseSettings): MONGODB_URL: str = "mongodb://localhost:27017" DATABASE_NAME: str = "rfcp" class Config: env_file = ".env" settings = Settings() ``` **app/core/database.py:** ```python from motor.motor_asyncio import AsyncIOMotorClient from app.core.config import settings class Database: client: AsyncIOMotorClient = None db = Database() async def get_database(): return db.client[settings.DATABASE_NAME] async def connect_to_mongo(): db.client = AsyncIOMotorClient(settings.MONGODB_URL) async def close_mongo_connection(): if db.client: db.client.close() ``` --- ### 4. Implement Models **app/models/site.py:** ```python from pydantic import BaseModel from typing import List, Optional class Sector(BaseModel): id: str name: str # Alpha, Beta, Gamma... power: float = 43 # dBm gain: float = 8 # dBi height: float = 30 # meters frequency: float = 1800 # MHz azimuth: float = 0 # degrees beamwidth: float = 65 # degrees tilt: float = 0 # degrees (electrical downtilt) antenna_type: str = "directional" class Site(BaseModel): id: str name: str lat: float lon: float sectors: List[Sector] = [] ``` **app/models/project.py:** ```python from pydantic import BaseModel, Field from typing import List, Optional from datetime import datetime from app.models.site import Site class CoverageSettings(BaseModel): radius: float = 10000 # meters resolution: float = 200 # meters min_signal: float = -105 # dBm max_signal: float = -65 # dBm class Project(BaseModel): id: Optional[str] = None name: str = "global" created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) sites: List[Site] = [] settings: CoverageSettings = Field(default_factory=CoverageSettings) ``` --- ### 5. Implement Routes **app/api/routes/health.py:** ```python from fastapi import APIRouter, Depends from app.core.database import get_database router = APIRouter() @router.get("/") async def health_check(): return {"status": "ok", "service": "rfcp-backend"} @router.get("/db") async def db_check(db = Depends(get_database)): try: await db.command("ping") return {"status": "ok", "database": "connected"} except Exception as e: return {"status": "error", "database": str(e)} ``` **app/api/routes/projects.py:** ```python from fastapi import APIRouter, Depends, HTTPException from typing import List from datetime import datetime from app.core.database import get_database from app.models.project import Project, CoverageSettings from app.models.site import Site router = APIRouter() @router.get("/current") async def get_current_project(db = Depends(get_database)): """Get the global project""" project = await db.projects.find_one({"name": "global"}) if not project: # Create default if doesn't exist default = Project(name="global") await db.projects.insert_one(default.model_dump()) return default project.pop("_id", None) return project @router.put("/current") async def update_current_project(project: Project, db = Depends(get_database)): """Update the global project""" project.updated_at = datetime.utcnow() data = project.model_dump() await db.projects.update_one( {"name": "global"}, {"$set": data}, upsert=True ) return project @router.get("/current/sites", response_model=List[Site]) async def get_sites(db = Depends(get_database)): """Get all sites""" project = await db.projects.find_one({"name": "global"}) if not project: return [] return project.get("sites", []) @router.put("/current/sites") async def update_sites(sites: List[Site], db = Depends(get_database)): """Update all sites""" await db.projects.update_one( {"name": "global"}, { "$set": { "sites": [s.model_dump() for s in sites], "updated_at": datetime.utcnow() } }, upsert=True ) return {"updated": len(sites)} @router.get("/current/settings") async def get_settings(db = Depends(get_database)): """Get coverage settings""" project = await db.projects.find_one({"name": "global"}) if not project: return CoverageSettings() return project.get("settings", CoverageSettings().model_dump()) @router.put("/current/settings") async def update_settings(settings: CoverageSettings, db = Depends(get_database)): """Update coverage settings""" await db.projects.update_one( {"name": "global"}, { "$set": { "settings": settings.model_dump(), "updated_at": datetime.utcnow() } }, upsert=True ) return settings ``` --- ### 6. Update main.py ```python from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.database import connect_to_mongo, close_mongo_connection from app.api.routes import health, projects @asynccontextmanager async def lifespan(app: FastAPI): await connect_to_mongo() yield await close_mongo_connection() app = FastAPI( title="RFCP Backend API", description="RF Coverage Planning Backend", version="1.1.0", lifespan=lifespan ) # CORS for frontend app.add_middleware( CORSMiddleware, allow_origins=["https://rfcp.eliah.one", "http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Routes app.include_router(health.router, prefix="/api/health", tags=["health"]) app.include_router(projects.router, prefix="/api/projects", tags=["projects"]) @app.get("/") async def root(): return {"message": "RFCP Backend API", "version": "1.1.0"} ``` --- ### 7. Restart Service & Test ```bash # Restart sudo systemctl restart rfcp-backend.service # Check logs journalctl -u rfcp-backend.service -f # Test endpoints curl http://10.10.10.1:8888/ curl http://10.10.10.1:8888/api/health/ curl http://10.10.10.1:8888/api/health/db curl http://10.10.10.1:8888/api/projects/current curl http://10.10.10.1:8888/docs # Swagger UI ``` --- ## ✅ Success Criteria - [ ] Service running without errors - [ ] `/api/health/` returns `{"status": "ok"}` - [ ] `/api/health/db` returns `{"database": "connected"}` - [ ] `/api/projects/current` returns project (creates if empty) - [ ] PUT `/api/projects/current/sites` saves sites to MongoDB - [ ] Swagger docs accessible at `/docs` --- ## 📝 Notes - MongoDB database: `rfcp` (separate from Open5GS) - No auth for now (VPN = security layer) - CORS configured for frontend - Pydantic v2 syntax (`model_dump()` not `dict()`) --- **Ready for Claude Code** 🚀