@mytec: before 1.1, last checks
This commit is contained in:
1254
RFCP-ARCHITECTURE.md
Normal file
1254
RFCP-ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load Diff
366
RFCP-Iteration-1.1-Backend-Foundation.md
Normal file
366
RFCP-Iteration-1.1-Backend-Foundation.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# 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** 🚀
|
||||||
@@ -249,7 +249,7 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
|||||||
value={customAzimuth}
|
value={customAzimuth}
|
||||||
onChange={(e) => setCustomAzimuth(e.target.value)}
|
onChange={(e) => setCustomAzimuth(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSetAzimuth()}
|
onKeyDown={(e) => e.key === 'Enter' && handleSetAzimuth()}
|
||||||
placeholder="0-359\u00B0"
|
placeholder="0-359°"
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user