- Dockerfile: multi-stage build (Node frontend + Python backend) - StartOS manifest, interfaces, backups, version graph - start.sh: auto-generates session secret, creates first approver on boot - Backend serves built frontend as SPA in production - Single data volume for SQLite DB, platform backups cover it
63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""Ten31Portal FastAPI application."""
|
|
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.staticfiles import StaticFiles
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
from starlette.responses import FileResponse
|
|
|
|
from ten31portal.config import SESSION_SECRET
|
|
from ten31portal.db_init import run_migrations
|
|
from ten31portal.routers.auth_router import router as auth_router
|
|
from ten31portal.routers.audit_router import router as audit_router
|
|
from ten31portal.routers.entity_router import router as entity_router
|
|
from ten31portal.routers.holding_router import router as holding_router
|
|
from ten31portal.routers.position_router import router as position_router
|
|
from ten31portal.routers.round_router import router as round_router
|
|
from ten31portal.routers.import_router import router as import_router
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
run_migrations()
|
|
yield
|
|
|
|
|
|
app = FastAPI(title="Ten31Portal", version="0.1.0", lifespan=lifespan)
|
|
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
|
|
app.include_router(auth_router)
|
|
app.include_router(audit_router)
|
|
app.include_router(entity_router)
|
|
app.include_router(holding_router)
|
|
app.include_router(position_router)
|
|
app.include_router(round_router)
|
|
app.include_router(import_router)
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
# Serve built frontend in production (when static/ dir exists next to the app)
|
|
_static_dir = Path(__file__).resolve().parent.parent / "static"
|
|
if _static_dir.is_dir():
|
|
@app.get("/{path:path}")
|
|
async def serve_spa(path: str):
|
|
file = _static_dir / path
|
|
if file.is_file():
|
|
return FileResponse(file)
|
|
return FileResponse(_static_dir / "index.html")
|
|
|
|
|
|
def cli() -> None:
|
|
import uvicorn
|
|
uvicorn.run("ten31portal.main:app", host="0.0.0.0", port=8000, reload=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|