f32203b630
- FastAPI + SQLite backend with routers for diagrams, devices, wires, bundles, connectors, export, and Octopart/Nexar search - Konva.js canvas: ortho/direct/curved routing, wire crossings, harness mode, snap-to-grid, multi-select, drag, resize, undo - Wire features: color/stripe, twisted pair helix, shielded glow, gauge size label, multi-core bundle grouping, length/unit tracking - Device library: connector, terminal block, cable, splice, label, group, relay, fuse, switch, component with custom connector support - Exports: BOM CSV, assembly TXT, JSON, formboard layout, PNG image - Auto-migration for schema changes via ALTER TABLE at startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
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, octopart, 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()
|
|
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(octopart.router, prefix="/api/octopart")
|
|
app.include_router(export.router, prefix="/api/export")
|
|
app.include_router(connectors.router, prefix="/api/connectors")
|
|
|
|
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)
|