Files
Remote-Terminal-for-MeshCore/app/main.py
2026-02-24 00:18:11 -08:00

114 lines
3.4 KiB
Python

import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.config import setup_logging
from app.database import db
from app.frontend_static import register_frontend_missing_fallback, register_frontend_static_routes
from app.radio import RadioDisconnectedError, radio_manager
from app.radio_sync import (
stop_message_polling,
stop_periodic_advert,
stop_periodic_sync,
)
from app.routers import (
channels,
contacts,
health,
messages,
packets,
radio,
read_state,
settings,
statistics,
ws,
)
setup_logging()
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage database and radio connection lifecycle."""
await db.connect()
logger.info("Database connected")
try:
await radio_manager.connect()
logger.info("Connected to radio")
await radio_manager.post_connect_setup()
except Exception as e:
logger.warning("Failed to connect to radio on startup: %s", e)
# Always start connection monitor (even if initial connection failed)
await radio_manager.start_connection_monitor()
yield
logger.info("Shutting down")
await radio_manager.stop_connection_monitor()
await stop_message_polling()
await stop_periodic_advert()
await stop_periodic_sync()
if radio_manager.meshcore:
await radio_manager.meshcore.stop_auto_message_fetching()
await radio_manager.disconnect()
await db.disconnect()
def _get_version() -> str:
"""Read version from pyproject.toml so it stays in sync automatically."""
try:
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
for line in pyproject.read_text().splitlines():
if line.startswith("version = "):
return line.split('"')[1]
except Exception:
pass
return "0.0.0"
app = FastAPI(
title="RemoteTerm for MeshCore API",
description="API for interacting with MeshCore mesh radio networks",
version=_get_version(),
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(RadioDisconnectedError)
async def radio_disconnected_handler(request: Request, exc: RadioDisconnectedError):
"""Return 503 when a radio disconnect race occurs during an operation."""
return JSONResponse(status_code=503, content={"detail": "Radio not connected"})
# API routes - all prefixed with /api for production compatibility
app.include_router(health.router, prefix="/api")
app.include_router(radio.router, prefix="/api")
app.include_router(contacts.router, prefix="/api")
app.include_router(channels.router, prefix="/api")
app.include_router(messages.router, prefix="/api")
app.include_router(packets.router, prefix="/api")
app.include_router(read_state.router, prefix="/api")
app.include_router(settings.router, prefix="/api")
app.include_router(statistics.router, prefix="/api")
app.include_router(ws.router, prefix="/api")
# Serve frontend static files in production
FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
if not register_frontend_static_routes(app, FRONTEND_DIR):
register_frontend_missing_fallback(app)