""" main.py - Thin FastAPI app: upload a PDF, run the pipeline, return the report. Run from the project root: uvicorn backend.main:app --reload The pipeline is synchronous and can take minutes on a large set; for a single architect checking one set at a time that's fine. Move to a job queue if this ever needs concurrency. """ import os import tempfile from typing import Optional from fastapi import FastAPI, UploadFile, File, Form, HTTPException from fastapi.responses import HTMLResponse, JSONResponse, Response from fastapi.staticfiles import StaticFiles from backend import config from backend.jobs import create_job, get_job from backend.pipeline.pdf_processor import render_page_jpeg app = FastAPI(title=config.APP_TITLE, version=config.APP_VERSION) _FRONTEND_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "frontend") @app.get("/health") def health(): return {"status": "ok", "model": config.MODEL, "key_configured": bool(config.AI_API_KEY), "email_configured": bool(config.SMTP_HOST and config.SMTP_USER and config.SMTP_PASSWORD)} @app.post("/check") async def check( file: UploadFile = File(...), notification_email: Optional[str] = Form(None), project_name: Optional[str] = Form(None), address: Optional[str] = Form(None), occupancy: Optional[str] = Form(None), work_type: Optional[str] = Form(None), text_local: bool = Form(False), ): """ Accept a PDF, start a background conflict check, and return a job_id immediately. The client can poll GET /jobs/{id} or just wait for the email. Optional intake fields (project_name/address/occupancy/work_type) feed the Stage 0 jurisdiction profile; anything left blank is derived from the cover sheet. """ if not file.filename.lower().endswith(".pdf"): raise HTTPException(status_code=400, detail="Please upload a PDF.") os.makedirs(config.UPLOAD_DIR, exist_ok=True) suffix = "_" + os.path.basename(file.filename) fd, tmp_path = tempfile.mkstemp(suffix=suffix, dir=config.UPLOAD_DIR) with os.fdopen(fd, "wb") as f: f.write(await file.read()) email = (notification_email or "").strip() or None project_input = { k: v.strip() for k, v in { "project_name": project_name, "address": address, "occupancy": occupancy, "work_type": work_type, }.items() if v and v.strip() } job_id = create_job(tmp_path, source_filename=file.filename, email=email, project_input=project_input, text_local=text_local) return JSONResponse({"job_id": job_id, "status": "queued", "email": email}) @app.get("/jobs/{job_id}") def job_status(job_id: str): job = get_job(job_id) if not job: raise HTTPException(status_code=404, detail="Job not found") return JSONResponse(job) @app.get("/jobs/{job_id}/sheet-image/{page}") def sheet_image(job_id: str, page: int): """Render one page of a completed job's source PDF as JPEG (sheet viewer).""" pdf = os.path.join(config.OUTPUT_DIR, job_id, "source.pdf") if not os.path.isfile(pdf): raise HTTPException(status_code=404, detail="Source PDF not found for this job") try: data = render_page_jpeg(pdf, page) except IndexError: raise HTTPException(status_code=404, detail=f"Page {page} out of range") except Exception as e: raise HTTPException(status_code=500, detail=f"Could not render page: {e}") return Response(content=data, media_type="image/jpeg", headers={"Cache-Control": "max-age=3600"}) @app.get("/", response_class=HTMLResponse) def index(): index_path = os.path.join(_FRONTEND_DIR, "index.html") if os.path.isfile(index_path): with open(index_path) as f: return f.read() return "
frontend/index.html not found.
" # Serve any other static assets (none required for the single-file UI). if os.path.isdir(_FRONTEND_DIR): app.mount("/static", StaticFiles(directory=_FRONTEND_DIR), name="static")