Files
rfcp/RFCP-Iteration-1.1-Backend-Foundation.md
2026-01-30 22:10:38 +02:00

367 lines
8.5 KiB
Markdown

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