@mytec: 1.1iter ready for testing

This commit is contained in:
2026-01-30 22:36:32 +02:00
parent 43b1267c56
commit 4326a6c4f7
28 changed files with 245 additions and 8 deletions

View File

@@ -5,7 +5,11 @@
"Bash(npm install:*)", "Bash(npm install:*)",
"Bash(npx tsc:*)", "Bash(npx tsc:*)",
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(npx eslint:*)" "Bash(npx eslint:*)",
"Bash(tree:*)",
"Bash(python:*)",
"Bash(pip --version:*)",
"Bash(pip install:*)"
] ]
} }
} }

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
data/terrain/ data/terrain/
data/terrain/ data/terrain/
venv/

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

5
backend/app/api/deps.py Normal file
View File

@@ -0,0 +1,5 @@
from app.core.database import get_database
async def get_db():
return await get_database()

View File

View File

@@ -0,0 +1,18 @@
from fastapi import APIRouter, Depends
from app.api.deps import get_db
router = APIRouter()
@router.get("/")
async def health_check():
return {"status": "ok", "service": "rfcp-backend", "version": "1.1.0"}
@router.get("/db")
async def db_check(db=Depends(get_db)):
try:
await db.command("ping")
return {"status": "ok", "database": "connected"}
except Exception as e:
return {"status": "error", "database": str(e)}

View File

@@ -0,0 +1,83 @@
from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime
from app.api.deps import get_db
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_db)):
"""Get the global project."""
project = await db.projects.find_one({"name": "global"})
if not project:
default = Project(name="global")
result = await db.projects.insert_one(default.model_dump())
return default.model_dump()
project.pop("_id", None)
return project
@router.put("/current")
async def update_current_project(project: Project, db=Depends(get_db)):
"""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 data
@router.get("/current/sites")
async def get_sites(db=Depends(get_db)):
"""Get all sites from the global project."""
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_db)):
"""Update all sites in the global project."""
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_db)):
"""Get coverage settings."""
project = await db.projects.find_one({"name": "global"})
if not project:
return CoverageSettings().model_dump()
return project.get("settings", CoverageSettings().model_dump())
@router.put("/current/settings")
async def update_settings(settings: CoverageSettings, db=Depends(get_db)):
"""Update coverage settings."""
await db.projects.update_one(
{"name": "global"},
{
"$set": {
"settings": settings.model_dump(),
"updated_at": datetime.utcnow(),
}
},
upsert=True,
)
return settings.model_dump()

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,13 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
MONGODB_URL: str = "mongodb://localhost:27017"
DATABASE_NAME: str = "rfcp"
TERRAIN_DATA_DIR: str = "/opt/rfcp/data/terrain"
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -0,0 +1,22 @@
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()

View File

@@ -1,29 +1,58 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
import os
app = FastAPI(title="RFCP API") from app.core.config import settings
from app.core.database import connect_to_mongo, close_mongo_connection
from app.api.routes import health, projects
# CORS
@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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["https://rfcp.eliah.one", "http://localhost:5173"], allow_origins=["https://rfcp.eliah.one", "http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
@app.get("/health") # Routes
async def health(): app.include_router(health.router, prefix="/api/health", tags=["health"])
return {"status": "healthy", "version": "1.0.0"} app.include_router(projects.router, prefix="/api/projects", tags=["projects"])
@app.get("/")
async def root():
return {"message": "RFCP Backend API", "version": "1.1.0"}
@app.get("/api/terrain/{region}") @app.get("/api/terrain/{region}")
async def get_terrain(region: str): async def get_terrain(region: str):
terrain_path = f"/opt/rfcp/data/terrain/{region}.hgt" """Serve SRTM terrain .hgt files."""
terrain_path = os.path.join(settings.TERRAIN_DATA_DIR, f"{region}.hgt")
if os.path.exists(terrain_path): if os.path.exists(terrain_path):
return FileResponse(terrain_path) return FileResponse(terrain_path)
return {"error": "Region not found"}, 404 return {"error": "Region not found"}, 404
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8090) uvicorn.run(app, host="0.0.0.0", port=8090)

View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,21 @@
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from app.models.site import Site
class CoverageSettings(BaseModel):
radius: float = 10000.0
resolution: float = 200.0
min_signal: float = -105.0
max_signal: float = -65.0
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)

View File

@@ -0,0 +1,35 @@
from pydantic import BaseModel
from typing import Optional
class Sector(BaseModel):
id: str
name: str = "Alpha"
power: float = 43.0
gain: float = 8.0
height: float = 30.0
frequency: float = 1800.0
azimuth: float = 0.0
beamwidth: float = 65.0
tilt: float = 0.0
antenna_type: str = "directional"
class Site(BaseModel):
id: str
name: str
lat: float
lon: float
height: float = 30.0
power: float = 43.0
gain: float = 8.0
frequency: float = 1800.0
antenna_type: str = "omni"
azimuth: Optional[float] = None
beamwidth: Optional[float] = None
tilt: Optional[float] = None
color: str = "#ef4444"
visible: bool = True
notes: Optional[str] = None
equipment: Optional[str] = None
sectors: list[Sector] = []

View File

@@ -2,3 +2,9 @@ fastapi==0.110.0
uvicorn[standard]==0.29.0 uvicorn[standard]==0.29.0
python-multipart==0.0.9 python-multipart==0.0.9
aiofiles==23.2.1 aiofiles==23.2.1
motor==3.3.2
pymongo==4.6.1
pydantic-settings==2.1.0
numpy==1.26.4
scipy==1.12.0
requests==2.31.0