211 lines
6.2 KiB
Python
211 lines
6.2 KiB
Python
"""
|
|
Terra-View - Unified monitoring platform for device fleets
|
|
Modular monolith architecture with strict feature boundaries
|
|
"""
|
|
import os
|
|
import logging
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Import configuration
|
|
from app.core.config import APP_NAME, VERSION, ENVIRONMENT
|
|
|
|
# Import UI routes
|
|
from app.ui import routes as ui_routes
|
|
|
|
# Import feature module routers (seismo)
|
|
from app.seismo.routers import (
|
|
roster as seismo_roster,
|
|
units as seismo_units,
|
|
photos as seismo_photos,
|
|
roster_edit as seismo_roster_edit,
|
|
dashboard as seismo_dashboard,
|
|
dashboard_tabs as seismo_dashboard_tabs,
|
|
activity as seismo_activity,
|
|
seismo_dashboard as seismo_seismo_dashboard,
|
|
settings as seismo_settings,
|
|
)
|
|
from app.seismo import routes as seismo_legacy_routes
|
|
|
|
# Import feature module routers (SLM)
|
|
from app.slm.routers import router as slm_router
|
|
from app.slm.dashboard import router as slm_dashboard_router
|
|
|
|
# Import API aggregation layer (placeholder for now)
|
|
from app.api import dashboard as api_dashboard
|
|
from app.api import roster as api_roster
|
|
|
|
# Initialize database tables
|
|
from app.seismo.database import engine as seismo_engine, Base as SeismoBase
|
|
SeismoBase.metadata.create_all(bind=seismo_engine)
|
|
|
|
from app.slm.database import engine as slm_engine, Base as SlmBase
|
|
SlmBase.metadata.create_all(bind=slm_engine)
|
|
|
|
# Initialize FastAPI app
|
|
app = FastAPI(
|
|
title=APP_NAME,
|
|
description="Unified monitoring platform for seismograph, modem, and sound level meter fleets",
|
|
version=VERSION
|
|
)
|
|
|
|
# Add validation error handler to log details
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
logger.error(f"Validation error on {request.url}: {exc.errors()}")
|
|
logger.error(f"Body: {await request.body()}")
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"detail": exc.errors()}
|
|
)
|
|
|
|
# Configure CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Mount static files
|
|
app.mount("/static", StaticFiles(directory="app/ui/static"), name="static")
|
|
|
|
# Middleware to add environment to request state
|
|
@app.middleware("http")
|
|
async def add_environment_to_context(request: Request, call_next):
|
|
"""Middleware to add environment variable to request state"""
|
|
request.state.environment = ENVIRONMENT
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
# ===== INCLUDE ROUTERS =====
|
|
|
|
# UI Layer (HTML pages)
|
|
app.include_router(ui_routes.router)
|
|
|
|
# Seismograph Feature Module APIs
|
|
app.include_router(seismo_roster.router)
|
|
app.include_router(seismo_units.router)
|
|
app.include_router(seismo_photos.router)
|
|
app.include_router(seismo_roster_edit.router)
|
|
app.include_router(seismo_dashboard.router)
|
|
app.include_router(seismo_dashboard_tabs.router)
|
|
app.include_router(seismo_activity.router)
|
|
app.include_router(seismo_seismo_dashboard.router)
|
|
app.include_router(seismo_settings.router)
|
|
app.include_router(seismo_legacy_routes.router)
|
|
|
|
# SLM Feature Module APIs
|
|
app.include_router(slm_router)
|
|
app.include_router(slm_dashboard_router)
|
|
|
|
# API Aggregation Layer (future cross-feature endpoints)
|
|
# app.include_router(api_dashboard.router) # TODO: Implement aggregation
|
|
# app.include_router(api_roster.router) # TODO: Implement aggregation
|
|
|
|
# ===== ADDITIONAL ROUTES FROM OLD MAIN.PY =====
|
|
# These will need to be migrated to appropriate modules
|
|
|
|
from fastapi.templating import Jinja2Templates
|
|
from typing import List, Dict
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
from fastapi import Depends
|
|
|
|
from app.seismo.database import get_db
|
|
from app.seismo.services.snapshot import emit_status_snapshot
|
|
from app.seismo.models import IgnoredUnit
|
|
|
|
# TODO: Move these to appropriate feature modules or UI layer
|
|
|
|
@app.post("/api/sync-edits")
|
|
async def sync_edits(request: dict, db: Session = Depends(get_db)):
|
|
"""Process offline edit queue and sync to database"""
|
|
# TODO: Move to seismo module
|
|
from app.seismo.models import RosterUnit
|
|
|
|
class EditItem(BaseModel):
|
|
id: int
|
|
unitId: str
|
|
changes: Dict
|
|
timestamp: int
|
|
|
|
class SyncEditsRequest(BaseModel):
|
|
edits: List[EditItem]
|
|
|
|
sync_request = SyncEditsRequest(**request)
|
|
results = []
|
|
synced_ids = []
|
|
|
|
for edit in sync_request.edits:
|
|
try:
|
|
unit = db.query(RosterUnit).filter_by(id=edit.unitId).first()
|
|
|
|
if not unit:
|
|
results.append({
|
|
"id": edit.id,
|
|
"status": "error",
|
|
"reason": f"Unit {edit.unitId} not found"
|
|
})
|
|
continue
|
|
|
|
for key, value in edit.changes.items():
|
|
if hasattr(unit, key):
|
|
if key in ['deployed', 'retired']:
|
|
setattr(unit, key, value in ['true', True, 'True', '1', 1])
|
|
else:
|
|
setattr(unit, key, value if value != '' else None)
|
|
|
|
db.commit()
|
|
|
|
results.append({
|
|
"id": edit.id,
|
|
"status": "success"
|
|
})
|
|
synced_ids.append(edit.id)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
results.append({
|
|
"id": edit.id,
|
|
"status": "error",
|
|
"reason": str(e)
|
|
})
|
|
|
|
synced_count = len(synced_ids)
|
|
|
|
return JSONResponse({
|
|
"synced": synced_count,
|
|
"total": len(sync_request.edits),
|
|
"synced_ids": synced_ids,
|
|
"results": results
|
|
})
|
|
|
|
|
|
@app.get("/health")
|
|
def health_check():
|
|
"""Health check endpoint"""
|
|
return {
|
|
"message": f"{APP_NAME} v{VERSION}",
|
|
"status": "running",
|
|
"version": VERSION,
|
|
"modules": ["seismo", "slm"]
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
from app.core.config import PORT
|
|
uvicorn.run(app, host="0.0.0.0", port=PORT)
|