diff --git a/backend/main.py b/backend/main.py index 2590081..4aa1279 100644 --- a/backend/main.py +++ b/backend/main.py @@ -96,21 +96,15 @@ async def lifespan(app: FastAPI): app = FastAPI(title="FreshScan AI", version="1.1.0", lifespan=lifespan) +# Parse ADDITIONAL_CORS_ORIGINS from environment +ADDITIONAL_CORS_ORIGINS = os.environ.get("ADDITIONAL_CORS_ORIGINS", "").split(",") _cors_origins = ( ["*"] if CORS_ALLOW_ALL else [ FRONTEND_URL, - # Current production frontend — always allow so a stale FRONTEND_URL - # env var doesn't lock out users. - "https://fresh-scanai.vercel.app", - # Extra comma-separated origins from env (e.g. preview deployments). - # ADDITIONAL_CORS_ORIGINS=https://preview.vercel.app,https://staging.example.com - *[ - o.strip() - for o in os.environ.get("ADDITIONAL_CORS_ORIGINS", "").split(",") - if o.strip() - ], + "https://fresh-scanai.vercel.app", # production frontend + *[origin.strip() for origin in ADDITIONAL_CORS_ORIGINS if origin.strip()], ] ) @@ -716,6 +710,22 @@ async def get_vendors(): except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) +@app.get("/api/v1/vendors/leaderboard") +async def get_vendor_leaderboard(): + """Get vendor leaderboard sorted by trust score""" + try: + resp = ( + _db() + .table("vendors") + .select("id, name, trust_score, total_scans, avg_freshness_score, lat, lng") + .order("trust_score", desc=True) + .limit(10) + .execute() + ) + return {"success": True, "leaderboard": resp.data} + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) + # ── MAP ─────────────────────────────────────────────────────────────────────── @@ -939,9 +949,10 @@ def _bwd_hook(_module, _grad_in, grad_out): from vendors import router as vendors_router, register_routes register_routes(vendors_router, _db) +from markets import router as markets_router +app.include_router(markets_router) app.include_router(vendors_router) - # ── ENTRY POINT ─────────────────────────────────────────────────────────────── if __name__ == "__main__": import uvicorn diff --git a/backend/markets.py b/backend/markets.py new file mode 100644 index 0000000..37f8fe8 --- /dev/null +++ b/backend/markets.py @@ -0,0 +1,113 @@ +""" +Real-world fish market locations via OpenStreetMap Overpass API. +Endpoint: GET /api/v1/maps/markets/live?lat=...&lng=...&radius=5000 +""" +import httpx +import math +from fastapi import APIRouter, Query + +router = APIRouter(prefix="/api/v1/maps/markets", tags=["markets"]) + +OVERPASS_URL = "https://overpass-api.de/api/interpreter" + +OVERPASS_QUERY_TEMPLATE = """ +[out:json][timeout:25]; +( + node["shop"="seafood"]({south},{west},{north},{east}); + node["shop"="fish"]({south},{west},{north},{east}); + node["amenity"="marketplace"]["name"~"fish|seafood|market",i]({south},{west},{north},{east}); + node["landuse"="retail"]["name"~"fish|seafood",i]({south},{west},{north},{east}); +); +out body; +""" + + +def _lat_lng_to_bbox(lat: float, lng: float, radius_m: float): + """Convert center + radius to bounding box (south, west, north, east).""" + delta_lat = radius_m / 111320 + delta_lng = radius_m / (111320 * abs(math.cos(math.radians(lat))) + 1e-9) + return { + "south": lat - delta_lat, + "west": lng - delta_lng, + "north": lat + delta_lat, + "east": lng + delta_lng, + } + + +def _parse_overpass(elements: list, fallback_score: int = 70) -> list: + """Normalize Overpass API elements into the MarketMapPage format.""" + markets = [] + for i, el in enumerate(elements): + tags = el.get("tags", {}) + name = ( + tags.get("name") + or tags.get("name:en") + or tags.get("shop") + or "Fish Market" + ) + lat = el.get("lat") + lon = el.get("lon") + if lat is None or lon is None: + continue + markets.append({ + "id": el.get("id", i + 1), + "name": name, + "score": fallback_score, + "lat": float(lat), + "lng": float(lon), + "vendors": 1, + "source": "openstreetmap", + "address": tags.get("addr:full") or tags.get("addr:street") or "", + }) + return markets + + +@router.get("/live") +async def get_live_markets( + lat: float = Query(..., description="Latitude of user location"), + lng: float = Query(..., description="Longitude of user location"), + radius: int = Query(default=5000, ge=500, le=50000, description="Search radius in meters"), +): + """ + Fetch real-world fish markets near a location using OpenStreetMap Overpass API. + Falls back to empty list if Overpass is unavailable. + """ + bbox = _lat_lng_to_bbox(lat, lng, radius) + query = OVERPASS_QUERY_TEMPLATE.format(**bbox) + try: + async with httpx.AsyncClient(timeout=30) as client: + response = await client.post( + OVERPASS_URL, + data={"data": query}, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "FreshScanAI/1.0 (https://github.com/jpdevhub/FreshScanAi)", + }, + ) + response.raise_for_status() + data = response.json() + elements = data.get("elements", []) + markets = _parse_overpass(elements) + return { + "success": True, + "source": "openstreetmap", + "count": len(markets), + "lat": lat, + "lng": lng, + "radius_m": radius, + "markets": markets, + } + except httpx.TimeoutException: + return { + "success": False, + "source": "openstreetmap", + "error": "Overpass API timed out. Try again or reduce radius.", + "markets": [], + } + except Exception as exc: + return { + "success": False, + "source": "openstreetmap", + "error": str(exc), + "markets": [], + }