Agri-weather dashboard for East African farms — weather, AI insights & tree-canopy analysis via a key-isolating backend proxy. Vite/React + Express/TypeScript, with caching, rate-limit awareness, and resilient fallbacks. Deployed on Render.
# 🌿 Agri-Weather Intelligence Dashboard
A weather, AI-insight, and tree-canopy-analysis dashboard for East-African farms,
built on the WeatherAI API.
It consumes all three meaningful API families — **weather/forecast**, **AI insights**
(with graceful degradation + Swahili), and **agroforestry tree analysis** — through a
backend proxy that keeps the API key off the browser entirely.
> **🌍 Live demo:** **
agri-weather-intelligence-d…
> **🔌 API proxy:**
agri-weather-server.onrende…
>
> ⏳ _Hosted on Render's free tier, so the backend **sleeps after ~15 min of
> inactivity** — the first request may take ~50s to cold-start, then it's fast.
> If a panel errors on first load, hit **Retry** once._
---
## Screenshots
## Architecture
```
┌──────────────────────────┐ ┌────────────────────────┐
Browser ──────▶ │ client (Vite + React) │ │ server (Express + TS) │
(no key) │ Render Static Site │ /api/* │ Render Web Service │
│ VITE_API_BASE_URL ───────┼────────▶│ injects Bearer wai_… │
└──────────────────────────┘ │ cache · rate tracking │
└───────────┬────────────┘
│ Authorization: Bearer
▼
api.weather-ai.co
```
The browser **never** holds the API key. It calls our proxy (`/api/...`); the Express
server injects `Authorization: Bearer wai_…` from a server-side env var and returns the
upstream response. The key never appears in the JS bundle or the network tab. This split —
a static frontend and a thin proxy backend — is the one architectural decision everything
else hangs off.
## Features
- **Current conditions + 7-day forecast** with a **daily/hourly toggle** (hourly is fetched
lazily, only when opened).
- **Opt-in AI insight** (`/v1/insights`): **off by default** to protect the scarce 200/mo AI
budget — the user clicks "Generate AI insight" to spend one. On the free plan the endpoint
is Pro/Scale-gated and returns `403`, which the card renders as a clean notic …