Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
]
)

Expand Down Expand Up @@ -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))

Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── MAP ───────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -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
Expand Down
113 changes: 113 additions & 0 deletions backend/markets.py
Original file line number Diff line number Diff line change
@@ -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": [],
}
Comment on lines +107 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sanitize exception messages to avoid information leakage.

Line 111 exposes the raw exception message via str(exc), which can reveal internal implementation details such as file paths, library versions, or database schema. Replace with a generic error message in production.

🛡️ Proposed sanitization
     except Exception as exc:
+        # Log the full exception internally for debugging
+        print(f"Overpass API error: {exc}")
         return {
             "success": False,
             "source": "openstreetmap",
-            "error": str(exc),
+            "error": "Failed to fetch markets. Please try again later.",
             "markets": [],
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as exc:
return {
"success": False,
"source": "openstreetmap",
"error": str(exc),
"markets": [],
}
except Exception as exc:
# Log the full exception internally for debugging
print(f"Overpass API error: {exc}")
return {
"success": False,
"source": "openstreetmap",
"error": "Failed to fetch markets. Please try again later.",
"markets": [],
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/markets.py` around lines 107 - 113, The except Exception as exc block
currently returns the raw exception via "error": str(exc); change this to return
a generic, non-sensitive message (e.g., "An internal error occurred") in
production while still recording the full exception to internal logs; update the
except block (the except Exception as exc handler and the returned dict with
"error") to check your environment/config (e.g., PRODUCTION or app.debug) and
return a sanitized message for clients, and call your logger (e.g.,
logger.exception or logger.error with exc_info=True) to preserve the original
exc for debugging.

Loading