from __future__ import annotations from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from card_store import CARD_TEMPLATES_DIR, NANOBOT_WORKSPACE from routes import ( cards_router, inbox_router, messages_router, rtc_router, sessions_router, templates_router, tools_router, workbench_router, ) from session_store import list_web_sessions, normalize_session_chat_id from web_runtime import create_web_runtime BASE_DIR = Path(__file__).resolve().parent DIST_DIR = BASE_DIR / "frontend" / "dist" NANOBOT_REPO_DIR = BASE_DIR.parent / "nanobot" NANOBOT_RUNTIME_WORKSPACE = ( (NANOBOT_WORKSPACE / "workspace") if (NANOBOT_WORKSPACE / "workspace").exists() else NANOBOT_WORKSPACE ) INBOX_DIR = NANOBOT_RUNTIME_WORKSPACE / "inbox" TOOL_JOB_TIMEOUT_SECONDS = 300.0 TOOL_JOB_RETENTION_SECONDS = 15 * 60 @asynccontextmanager async def app_lifespan(app: FastAPI): runtime = create_web_runtime( repo_dir=NANOBOT_REPO_DIR, inbox_dir=INBOX_DIR, tool_job_timeout_seconds=TOOL_JOB_TIMEOUT_SECONDS, tool_job_retention_seconds=TOOL_JOB_RETENTION_SECONDS, list_sessions=list_web_sessions, normalize_chat_id=normalize_session_chat_id, ) app.state.runtime = runtime try: yield finally: await runtime.shutdown() def create_app() -> FastAPI: app = FastAPI(title="Nanobot SuperTonic Wisper Web", lifespan=app_lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") async def health() -> JSONResponse: return JSONResponse({"status": "ok"}) app.include_router(tools_router) app.include_router(sessions_router) app.include_router(cards_router) app.include_router(workbench_router) app.include_router(inbox_router) app.include_router(templates_router) app.include_router(messages_router) app.include_router(rtc_router) if DIST_DIR.exists(): assets_dir = DIST_DIR / "assets" if assets_dir.exists(): app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets") if CARD_TEMPLATES_DIR.exists(): app.mount( "/card-templates", StaticFiles(directory=str(CARD_TEMPLATES_DIR)), name="card-templates", ) @app.get("/{full_path:path}") async def spa_fallback(full_path: str) -> FileResponse: candidate = DIST_DIR / full_path if candidate.is_file(): return FileResponse(str(candidate)) response = FileResponse(str(DIST_DIR / "index.html")) response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response return app app = create_app()