Files
rfcp/docs/devlog/back/RFCP-Iteration-1.1-Backend-Foundation.md

8.5 KiB

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

# 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

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:

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:

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:

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:

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:

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:

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

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

# 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 🚀