| from typing import Literal |
|
|
| import json |
| from datetime import datetime, timezone |
| from pathlib import Path |
|
|
| from fastapi import FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel, Field |
| from fastapi.responses import FileResponse |
| from fastapi.staticfiles import StaticFiles |
| from starlette.concurrency import run_in_threadpool |
|
|
| from core.config import settings |
| from core.logger import logger, setup_logger |
| from api.realtime import manager |
| from inspection.report_repository import ( |
| get_analytics_summary, |
| get_latest_report, |
| list_reports, |
| ) |
| from inspection.service import inspect_base64_image |
|
|
|
|
| class InspectionRequest(BaseModel): |
| image_base64: str = Field(..., min_length=10) |
| filename: str | None = None |
| source: str = "upload" |
| persist: bool = True |
| llm_mode: Literal["off", "auto", "always"] = "off" |
|
|
|
|
| setup_logger() |
|
|
| FRONTEND_DIST_DIR = Path(settings.FRONTEND_DIST) |
| FRONTEND_INDEX = FRONTEND_DIST_DIR / "index.html" |
|
|
|
|
| app = FastAPI( |
| title="Manufacturing Monitoring System API", |
| version="2.0.0", |
| ) |
|
|
| |
| |
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=list(settings.CORS_ORIGINS), |
| allow_credentials=False, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| |
| |
| @app.get("/") |
| def root(): |
| if FRONTEND_INDEX.exists(): |
| return FileResponse(FRONTEND_INDEX) |
| return { |
| "message": "Manufacturing Monitoring System API running", |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| "origins": list(settings.CORS_ORIGINS), |
| } |
|
|
|
|
| @app.get("/health") |
| def health(): |
| return { |
| "status": "ok", |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| } |
|
|
|
|
| |
| |
| |
| @app.get("/reports") |
| def get_reports(limit: int = Query(default=settings.REPORT_FETCH_LIMIT, ge=1, le=200)): |
| return list_reports(limit=limit) |
|
|
|
|
| |
| |
| |
| @app.get("/reports/latest") |
| def get_latest(): |
| latest_report = get_latest_report() |
| return latest_report or {} |
|
|
|
|
| @app.get("/analytics/summary") |
| def analytics_summary(limit: int = Query(default=120, ge=1, le=500)): |
| return get_analytics_summary(limit=limit) |
|
|
|
|
| @app.post("/inspect/image") |
| async def inspect_image(payload: InspectionRequest): |
| try: |
| return await run_in_threadpool( |
| inspect_base64_image, |
| payload.image_base64, |
| source=payload.source or "upload", |
| metadata={"filename": payload.filename}, |
| persist=payload.persist, |
| llm_mode=payload.llm_mode, |
| ) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail=str(exc)) from exc |
| except Exception as exc: |
| logger.exception("Image inspection failed") |
| raise HTTPException(status_code=500, detail="Image inspection failed") from exc |
|
|
|
|
| @app.post("/inspect/frame") |
| async def inspect_frame(payload: InspectionRequest): |
| try: |
| return await run_in_threadpool( |
| inspect_base64_image, |
| payload.image_base64, |
| source=payload.source or "camera", |
| metadata={"filename": payload.filename}, |
| persist=payload.persist, |
| llm_mode=payload.llm_mode, |
| ) |
| except ValueError as exc: |
| raise HTTPException(status_code=400, detail=str(exc)) from exc |
| except Exception as exc: |
| logger.exception("Camera frame inspection failed") |
| raise HTTPException(status_code=500, detail="Camera frame inspection failed") from exc |
|
|
|
|
| |
| |
| |
| @app.websocket("/ws") |
| async def websocket_endpoint(websocket: WebSocket): |
| await manager.connect(websocket) |
| latest_report = await run_in_threadpool(get_latest_report) |
|
|
| if latest_report: |
| await manager.send_json( |
| websocket, |
| { |
| "type": "inspection.snapshot", |
| **latest_report, |
| }, |
| ) |
| else: |
| await manager.send_json( |
| websocket, |
| { |
| "type": "connection.ready", |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| }, |
| ) |
|
|
| try: |
| while True: |
| message = await websocket.receive_text() |
|
|
| try: |
| payload = json.loads(message) |
| except json.JSONDecodeError: |
| payload = {"type": message} |
|
|
| if payload.get("type") == "ping": |
| await manager.send_json( |
| websocket, |
| { |
| "type": "pong", |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| }, |
| ) |
| except WebSocketDisconnect: |
| await manager.disconnect(websocket) |
| except Exception as exc: |
| logger.warning(f"WebSocket connection closed unexpectedly: {exc}") |
| await manager.disconnect(websocket) |
|
|
|
|
| if FRONTEND_DIST_DIR.exists(): |
| assets_dir = FRONTEND_DIST_DIR / "assets" |
| if assets_dir.exists(): |
| app.mount("/assets", StaticFiles(directory=assets_dir), name="frontend-assets") |
|
|
| @app.get("/{full_path:path}", include_in_schema=False) |
| async def frontend_routes(full_path: str): |
| candidate = FRONTEND_DIST_DIR / full_path |
| if candidate.is_file(): |
| return FileResponse(candidate) |
| if FRONTEND_INDEX.exists(): |
| return FileResponse(FRONTEND_INDEX) |
| raise HTTPException(status_code=404, detail="Not found") |
|
|