@mytec: 1.1iter ready for testing
This commit is contained in:
@@ -5,7 +5,11 @@
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npx eslint:*)"
|
||||
"Bash(npx eslint:*)",
|
||||
"Bash(tree:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(pip --version:*)",
|
||||
"Bash(pip install:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
data/terrain/
|
||||
data/terrain/
|
||||
venv/
|
||||
|
||||
BIN
backend/app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/app/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/main.cpython-311.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/deps.cpython-311.pyc
Normal file
BIN
backend/app/api/__pycache__/deps.cpython-311.pyc
Normal file
Binary file not shown.
5
backend/app/api/deps.py
Normal file
5
backend/app/api/deps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from app.core.database import get_database
|
||||
|
||||
|
||||
async def get_db():
|
||||
return await get_database()
|
||||
0
backend/app/api/routes/__init__.py
Normal file
0
backend/app/api/routes/__init__.py
Normal file
BIN
backend/app/api/routes/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/app/api/routes/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/routes/__pycache__/health.cpython-311.pyc
Normal file
BIN
backend/app/api/routes/__pycache__/health.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/routes/__pycache__/projects.cpython-311.pyc
Normal file
BIN
backend/app/api/routes/__pycache__/projects.cpython-311.pyc
Normal file
Binary file not shown.
18
backend/app/api/routes/health.py
Normal file
18
backend/app/api/routes/health.py
Normal 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)}
|
||||
83
backend/app/api/routes/projects.py
Normal file
83
backend/app/api/routes/projects.py
Normal 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()
|
||||
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
BIN
backend/app/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/app/core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/config.cpython-311.pyc
Normal file
BIN
backend/app/core/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/database.cpython-311.pyc
Normal file
BIN
backend/app/core/__pycache__/database.cpython-311.pyc
Normal file
Binary file not shown.
13
backend/app/core/config.py
Normal file
13
backend/app/core/config.py
Normal 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()
|
||||
22
backend/app/core/database.py
Normal file
22
backend/app/core/database.py
Normal 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()
|
||||
@@ -1,29 +1,58 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
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(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://rfcp.eliah.one", "http://localhost:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "version": "1.0.0"}
|
||||
# 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"}
|
||||
|
||||
|
||||
@app.get("/api/terrain/{region}")
|
||||
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):
|
||||
return FileResponse(terrain_path)
|
||||
return {"error": "Region not found"}, 404
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8090)
|
||||
|
||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/project.cpython-311.pyc
Normal file
BIN
backend/app/models/__pycache__/project.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/site.cpython-311.pyc
Normal file
BIN
backend/app/models/__pycache__/site.cpython-311.pyc
Normal file
Binary file not shown.
21
backend/app/models/project.py
Normal file
21
backend/app/models/project.py
Normal 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)
|
||||
35
backend/app/models/site.py
Normal file
35
backend/app/models/site.py
Normal 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] = []
|
||||
@@ -2,3 +2,9 @@ fastapi==0.110.0
|
||||
uvicorn[standard]==0.29.0
|
||||
python-multipart==0.0.9
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user