from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.routing import Route
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.api import ingest, servers
from app.db import close_db, init_db
from app.mcp import mcp, mcp_asgi
# Собранный Vue-фронт (panel/frontend/dist) попадает сюда в docker-образе;
# в dev фронт крутится отдельно (vite, proxy /api → localhost:8000)
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
@asynccontextmanager
async def lifespan(_: FastAPI):
await init_db()
# lifespan смонтированных sub-app не вызывается — MCP session manager
# стартуем здесь, иначе /mcp отвечает 500 ("Task group is not initialized")
async with mcp.session_manager.run():
yield
await close_db()
app = FastAPI(title="GHard Monitor API", version="0.1.0", lifespan=lifespan)
# В проде фронт раздаётся тем же origin; CORS нужен для vite dev-сервера (этап 3)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(ingest.router)
app.include_router(servers.router)
# MCP для ИИ-агентов (streamable HTTP, stateless) — POST/GET/DELETE /mcp
app.router.routes.append(
Route("/mcp", mcp_asgi, methods=["GET", "POST", "DELETE"])
)
@app.get("/api/v1/health")
async def health() -> dict:
return {"status": "ok"}
# --- Статика фронтенда: SPA с fallback на index.html -------------------------
if STATIC_DIR.is_dir():
assets = STATIC_DIR / "assets"
if assets.is_dir():
app.mount("/assets", StaticFiles(directory=assets), name="assets")
@app.get("/{full_path:path}", include_in_schema=False)
async def spa(full_path: str) -> FileResponse:
"""Роуты Vue отдаём из dist, всё непонятное — index.html (SPA history)."""
candidate = STATIC_DIR / full_path
if full_path and candidate.is_file():
return FileResponse(candidate)
return FileResponse(STATIC_DIR / "index.html")