""" 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 ( nl43_proxy as slm_nl43_proxy, dashboard as slm_dashboard, ui as slm_ui, ) # 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) # 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_nl43_proxy.router) app.include_router(slm_dashboard.router) app.include_router(slm_ui.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)