import os from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from sqlalchemy import text, inspect as sa_inspect from .database import Base, engine, SessionLocal from .routers import bundles, connectors, diagrams, devices, export, formboard, git_mgr, octopart, views, wires Base.metadata.create_all(bind=engine) # Lightweight column migrations (SQLite ALTER TABLE IF NOT EXISTS unsupported) def _migrate(): import sys # ── Column additions ──────────────────────────────────────────────────────── try: with engine.connect() as conn: def _cols(table): return {c["name"] for c in sa_inspect(engine).get_columns(table)} if "waypoints" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN waypoints TEXT DEFAULT '[]'")) conn.commit() if "sheet_id" not in _cols("devices"): conn.execute(text("ALTER TABLE devices ADD COLUMN sheet_id INTEGER REFERENCES sheets(id)")) conn.commit() if "sheet_id" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN sheet_id INTEGER REFERENCES sheets(id)")) conn.commit() if "twisted_pair" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN twisted_pair INTEGER DEFAULT 0")) conn.commit() if "twist_pitch" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN twist_pitch REAL DEFAULT 16.0")) conn.commit() if "shielded" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN shielded INTEGER DEFAULT 0")) conn.commit() if "bundle_id" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN bundle_id INTEGER REFERENCES wire_bundles(id)")) conn.commit() if "show_size_label" not in _cols("wires"): conn.execute(text("ALTER TABLE wires ADD COLUMN show_size_label INTEGER DEFAULT 0")) conn.commit() # diagram_views / view_device_positions / view_wire_waypoints are created # via Base.metadata.create_all — no ALTER TABLE needed for new tables except Exception as e: print(f"[migrate] column error: {e}", file=sys.stderr) _migrate() app = FastAPI(title="Wiring Diagram Maker", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) app.include_router(bundles.router, prefix="/api/bundles") app.include_router(diagrams.router, prefix="/api/diagrams") app.include_router(devices.router, prefix="/api/devices") app.include_router(wires.router, prefix="/api/wires") app.include_router(views.router, prefix="/api/views") app.include_router(octopart.router, prefix="/api/octopart") app.include_router(export.router, prefix="/api/export") app.include_router(connectors.router, prefix="/api/connectors") app.include_router(formboard.router, prefix="/api/formboard") app.include_router(git_mgr.router, prefix="/api/git") FRONTEND_DIR = os.path.normpath( os.path.join(os.path.dirname(__file__), "..", "..", "frontend") ) if os.path.isdir(FRONTEND_DIR): for _sub in ("css", "js"): _d = os.path.join(FRONTEND_DIR, _sub) if os.path.isdir(_d): app.mount(f"/{_sub}", StaticFiles(directory=_d), name=_sub) _index = os.path.join(FRONTEND_DIR, "index.html") @app.get("/") async def _root(): return FileResponse(_index) @app.exception_handler(404) async def _spa_fallback(request: Request, exc): if request.url.path.startswith("/api"): return JSONResponse(status_code=404, content={"detail": "Not Found"}) return FileResponse(_index)