/ GET /current-aqi, GET /source-attribution Geospatial Pollution Source Attribution /map GET /current-aqi, GET /source-attribution Hyperlocal AQI Forecasting (24–72h) /forecast GET /forecast Enforcement Intelligence /enforcement GET /enforcement Multi-City Comparative Intelligence /compare GET /multi-city Citizen Health Risk Advisory /advisory GET /citizen-advisoryapp/data/. Tomorrow, the same function can call a live provider (CPCB, OpenAQ, IMD, Sentinel-5P, ERA5 — see §10) instead — the router and schema on either side of the service never change:frontend/src/server.ts) has no relationship to the FastAPI backend — it only server-renders the React app. All data fetching happens client-side (and during SSR passes) via plain fetch calls to the FastAPI service.aqiCategory, aqiColor) which stay client-side since they're deterministic and instant (no reason to round-trip to a server to pick a color).Router -> Service -> Cached Dataset. Routers stay thin (parse request, call one service function, return the schema); each app/services/*_service.py module is the seam where a live provider will be plugged in later without touching the router or the Pydantic schema. See Future Live Integrations below.frontend/src/hooks/.useState, unchanged from the original scaffold.Field(alias=...)) match the frontend's original TypeScript interfaces exactly, so no response reshaping happens anywhere in the frontend.bun.lock; npm works equally well and is what these instructions use).http://localhost:5173).uvicorn app.main:app --reload --port 8000) — the frontend has nothing to render without it.npm run dev inside frontend/).VITE_API_BASE_URL in frontend/.env.local to match, and add the frontend's origin to CORS_ORIGINS in backend/.env.http://localhost:8000 (configurable). All endpoints are GET, unauthenticated, and return JSON.GET /healthGET /current-aqiGET /forecastGET /source-attributionGET /enforcementGET /multi-cityGET /citizen-advisoryaqi override → ward's live station reading → city-wide average.aqi int (0–500) Override AQI value ward string Ward/district name; resolves to that ward's live station reading 404 if ward doesn't match any known station./docs while the backend is running.useState for UI state, TanStack Query for everything server-derived.backend/app/data/*.py) rather than real sensor/satellite feeds. Every field that represents a satellite or meteorological reading is clearly labeled as such in code comments and docstrings — this is explicitly acceptable per the hackathon problem statement./multi-city additionally covers Mumbai, Bengaluru, Chennai, Hyderabad, Kolkata, Pune, Ahmedabad, Jaipur, Lucknow, and Dehradun./citizen-advisory's ward parameter exists on the backend but isn't yet exposed as a UI control. The endpoint supports ward-specific advisories (?ward=East%20Delhi), tested and working, but the frontend's advisory page currently only exposes the AQI slider — adding a ward selector would be a small, additive follow-up, not a redesign.wardForecasts and wardAttribution (hyperlocal, per-ward breakdowns for forecast and source attribution) are fully implemented and returned by the backend but not yet rendered in the frontend UI, which currently shows only the city-wide series/breakdown for those two pages. This was a deliberate scope decision made during incremental integration to avoid UI changes mid-build; the data is available on each hook's data object for a follow-up UI pass.placeholderData keeps the previous advisory list visible during that window so there's no flash.allow_methods=["*"], allow_headers=["*"]) but restricted to explicit origins via CORS_ORIGINS — appropriate for a local hackathon demo, would need tightening for any real deployment.app/services/*_service.py module is structured so it maps cleanly to a future live-provider client without touching schemas or routers — see below.frontend/src/hooks/ calls a fixed REST contract (GET /current-aqi, GET /forecast, etc.) and every Pydantic schema in backend/app/schemas/ defines that contract's exact shape, field names, and aliases. As long as a service function keeps returning that same shape, the frontend has no way to tell whether the data underneath came from a cached dataset or a live feed.backend/app/services/*_service.py module is a documented seam: today its one function reads a static dataset from backend/app/data/; in the future, that same function's body is swapped for a call to a live provider client (e.g. a cpcb_client.fetch_live_stations()), while its return shape — and therefore its router and schema — stays untouched. The Future Integration: comment block at the top of every service file names the specific provider(s) that function is expected to call.backend/app/data/ is a static, deterministic Python dataset (India-only: Delhi station/ward data plus the Mumbai/Bengaluru/Chennai/Hyderabad/Kolkata/Pune/Ahmedabad/Jaipur/Lucknow/Dehradun multi-city table) — it doesn't reshuffle between requests, and the app runs fully offline with zero network calls.*_service.py function, keep the same return dict shape. No router, schema, or frontend change is required, and every endpoint can be migrated independently and incrementally.current_aqi_service.py GET /current-aqi CPCB (live station network), OpenAQ (supplementary coverage) forecast_service.py GET /forecast IMD (weather forecast), ERA5 (reanalysis meteorology), Sentinel-5P (satellite trace-gas input), CPCB (ground-truth baseline) source_attribution_service.py GET /source-attribution Sentinel-5P (satellite NO₂/AOD), CPCB (calibration), OpenAQ (supplementary coverage) enforcement_service.py GET /enforcement CPCB (exceedance/inspection records), Sentinel-5P (supporting evidence) multi_city_service.py GET /multi-city CPCB (per-city aggregation), OpenAQ (supplementary coverage) citizen_advisory_service.py GET /citizen-advisory CPCB (live ward reading), IMD (weather-adjusted severity)Posted Sep 2, 2026
Developed an MVP platform for air quality intelligence with decoupled frontend and backend.
0
1