File size: 22,481 Bytes
13fc29d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 | # SolarWine β Production Architecture
## Overview
Migrate from a single-process Streamlit prototype to a 3-layer production system
deployed on free-tier infrastructure. The monorepo stays β no new repositories.
```
Baseline/ (monorepo)
βββ frontend/ NEW β Lovable-generated React app
β βββ src/
β β βββ pages/
β β β βββ Dashboard.tsx Overview + energy + weather
β β β βββ Advisor.tsx Chatbot + advisory cards
β β β βββ Control.tsx System status + tracker angles
β β β βββ Photosynthesis.tsx FvCB/ML results + forecasts
β β β βββ Shading.tsx Interactive simulator
β β β βββ Docs.tsx Documentation
β β βββ components/
β β β βββ EnergyCard.tsx
β β β βββ WeatherCard.tsx
β β β βββ Chatbot.tsx
β β β βββ TrackerStatus.tsx
β β β βββ Charts.tsx Recharts / Plotly wrappers
β β βββ hooks/
β β β βββ useWeather.ts fetch /api/weather/*
β β β βββ useEnergy.ts fetch /api/energy/*
β β β βββ useSensors.ts fetch /api/sensors/*
β β β βββ useChatbot.ts POST /api/chatbot/message
β β β βββ useControl.ts fetch /api/control/*
β β βββ lib/
β β β βββ api.ts API base URL + fetch wrapper
β β βββ App.tsx Router + layout
β βββ package.json
β βββ tailwind.config.js
β
βββ backend/ NEW β FastAPI middleware + background services
β βββ api/
β β βββ __init__.py
β β βββ main.py FastAPI app, CORS, lifespan
β β βββ routes/
β β β βββ weather.py GET /api/weather/*
β β β βββ sensors.py GET /api/sensors/*
β β β βββ energy.py GET /api/energy/*
β β β βββ photosynthesis.py GET /api/photosynthesis/*
β β β βββ control.py GET /api/control/*
β β β βββ chatbot.py POST /api/chatbot/message
β β β βββ health.py GET /api/health
β β βββ deps.py Shared dependencies (DataHub, Redis)
β β βββ auth.py JWT middleware (Phase 11.6)
β βββ workers/
β β βββ control_tick.py ControlLoop 15-min cron entry point
β β βββ daily_planner.py DayAheadPlanner daily cron entry point
β βββ Dockerfile
β βββ requirements.txt
β
βββ .github/
β βββ workflows/
β βββ control-tick.yml GitHub Actions: 15-min cron
β βββ daily-planner.yml GitHub Actions: daily 05:00 IST cron
β
βββ src/ UNCHANGED β shared Python core
β βββ control_loop.py
β βββ command_arbiter.py
β βββ tracker_dispatcher.py
β βββ day_ahead_planner.py
β βββ energy_budget.py
β βββ data/
β β βββ data_providers.py DataHub + 6 services
β β βββ thingsboard_client.py
β β βββ ims_client.py
β βββ chatbot/
β β βββ vineyard_chatbot.py
β β βββ guardrails.py
β β βββ routing_agent.py
β βββ models/
β β βββ farquhar_model.py
β β βββ phenology.py
β βββ forecasting/
β β βββ predictor.py
β β βββ ts_predictor.py
β βββ shading/
β β βββ solar_geometry.py
β β βββ tradeoff_engine.py
β βββ ...
β
βββ config/settings.py UNCHANGED β shared configuration
βββ app.py KEPT temporarily (Streamlit, deprecated)
βββ ui/ KEPT temporarily (Streamlit tabs, deprecated)
βββ docker-compose.yml NEW β local dev orchestration
βββ requirements.txt Shared Python deps
```
---
## Layer 1 β The Brain (Backend Services)
### What it is
All business logic, models, data access, and control algorithms. This layer
already exists in `src/`. The only change is extracting the runtime entry points
into `backend/workers/`.
### Components
| Component | Module | Runtime | Schedule |
|-----------|--------|---------|----------|
| ControlLoop | `src/control_loop.py` | GitHub Actions cron | Every 15 min |
| DayAheadPlanner | `src/day_ahead_planner.py` | GitHub Actions cron | Daily 05:00 IST + re-plan events |
| DataHub | `src/data/data_providers.py` | Imported by API + workers | Always available |
| ThingsBoard client | `src/data/thingsboard_client.py` | Imported by DataHub | On-demand |
| IMS client | `src/data/ims_client.py` | Imported by DataHub | On-demand (30-min TTL) |
| FvCB model | `src/models/farquhar_model.py` | Imported by PhotosynthesisService | On-demand (~10ms) |
| ML ensemble | `src/forecasting/predictor.py` | Imported by PhotosynthesisService | On-demand (~500ms) |
| Chronos forecaster | `src/forecasting/ts_predictor.py` | Imported by PhotosynthesisService | On-demand (~2-5s) |
| CommandArbiter | `src/command_arbiter.py` | Imported by ControlLoop | Per tick |
| TrackerDispatcher | `src/tracker_dispatcher.py` | Imported by ControlLoop | Per tick |
| EnergyBudget | `src/energy_budget.py` | Imported by ControlLoop | Per tick |
| Chatbot + guardrails | `src/chatbot/*.py` | Imported by API route | Per message |
### Caching strategy
| Data | Current | Production |
|------|---------|------------|
| IMS weather | In-memory TTLCache (30 min) | **Upstash Redis** (30 min TTL) |
| Vine sensors | In-memory TTLCache (5 min) | **Upstash Redis** (5 min TTL) |
| Energy daily | No cache | **Upstash Redis** (15 min TTL) |
| ML trained model | Pickle file | Pickle in `/tmp` + Redis metadata |
| Day-ahead plan | JSON file | Redis hash + JSON file backup |
| Chatbot history | Streamlit session_state | Redis list per session |
### External dependencies
| Service | URL | Auth | Purpose |
|---------|-----|------|---------|
| ThingsBoard | `web.seymouragri.com` | Username/password β JWT | 21 sensors, 4 trackers, energy |
| IMS | `api.ims.gov.il` | API token | Weather station 43 (Sde Boker) |
| Google Gemini | `generativelanguage.googleapis.com` | API key | Chatbot (2.5-flash), routing |
| Upstash Redis | `*.upstash.io` | Token | Shared cache |
---
## Layer 2 β Middleware (FastAPI on HuggingFace Spaces)
### What it is
A thin Python API layer that wraps `DataHub` services as REST endpoints. Hosted
on **HuggingFace Spaces** (Docker SDK, free tier) β Python-native, no cold starts
on the free tier's persistent container.
### Tech stack
| Component | Technology | Why |
|-----------|-----------|-----|
| Framework | **FastAPI** | Same Python team, async, auto-docs, WebSocket |
| Serialization | **Pydantic v2** | Already a FastAPI dependency, typed responses |
| Auth | **JWT** (PyJWT) | Simple, stateless, no external IdP needed initially |
| Rate limiting | **slowapi** | Token bucket per IP, protects Gemini quota |
| CORS | FastAPI `CORSMiddleware` | Allow Cloudflare Pages frontend origin |
| WebSocket | FastAPI WebSocket | Live tracker angles, energy push (future) |
| Process manager | **uvicorn** | ASGI server, production-ready |
| Hosting | **HuggingFace Spaces** (Docker SDK) | Free, Python-native, persistent container |
### HuggingFace Space configuration
The API Space uses Docker SDK. A `Dockerfile` at `backend/` builds the FastAPI
server. The Space README metadata:
```yaml
---
title: SolarWine API
emoji: πΏ
colorFrom: green
colorTo: yellow
sdk: docker
app_port: 7860
private: true
---
```
Note: HF Spaces exposes port 7860, so FastAPI binds to `0.0.0.0:7860`.
### API endpoints
```
Health
GET /api/health β { status, uptime, redis_ok, tb_ok }
Weather
GET /api/weather/current β WeatherService.get_current()
GET /api/weather/history?days=7 β WeatherService.get_history()
GET /api/weather/forecast β WeatherService.get_forecast()
Vine Sensors
GET /api/sensors/snapshot β VineSensorService.get_snapshot()
GET /api/sensors/history?type=crop&hours=24 β VineSensorService.get_history()
Energy
GET /api/energy/current β EnergyService.get_current()
GET /api/energy/daily/{date} β EnergyService.get_daily_production()
GET /api/energy/history?hours=24 β EnergyService.get_history()
GET /api/energy/predict/{date} β EnergyService.predict()
Photosynthesis
GET /api/photosynthesis/current β PhotosynthesisService.get_current()
GET /api/photosynthesis/forecast β PhotosynthesisService.forecast_day_ahead()
Control
GET /api/control/status β Last TickResult from Redis
GET /api/control/plan β Current DayAheadPlan from Redis
GET /api/control/budget β EnergyBudget current state
GET /api/control/trackers β Live tracker angles from TB
Chatbot
POST /api/chatbot/message β VineyardChatbot.chat()
Body: { message, session_id }
Response: { message, confidence, sources, caveats, rule_violations }
POST /api/chatbot/feedback β feedback.log_feedback()
Body: { session_id, message_id, rating, comment }
Biology
GET /api/biology/phenology β BiologyService current stage
GET /api/biology/rules β All biology rules
```
### Middleware stack (order matters)
1. **CORS** β allow `*.pages.dev` + localhost
2. **Rate limiter** β 60 req/min general, 10 req/min chatbot (Gemini costs)
3. **Auth** β JWT validation (initially optional, enforced in Phase 11.6)
4. **Request logging** β structured JSON logs
5. **Error handler** β consistent error response format
---
## Layer 3 β Frontend (Lovable β Cloudflare Pages)
### What it is
Production-grade React frontend generated via **Lovable** (AI-powered React
builder). Produces a standard Vite + React + TypeScript + Tailwind project.
Deployed to **Cloudflare Pages** (free tier, unlimited bandwidth, private org repos supported).
Lovable generates the initial scaffold and components rapidly. After generation,
the code is committed to the monorepo under `frontend/` and maintained manually
or with further Lovable iterations.
### Tech stack
| Component | Technology | Why |
|-----------|-----------|-----|
| Generator | **Lovable** | AI-generated React UI, rapid prototyping |
| Framework | **React 19 + Vite** | Lovable default output, fast build |
| Language | **TypeScript** | Type safety, IDE support |
| Styling | **Tailwind CSS + shadcn/ui** | Lovable default, beautiful components |
| Charts | **Recharts** (default) or **Plotly.js** | Data visualization |
| HTTP | **TanStack Query** (React Query) | Auto-refresh, caching, loading states |
| Routing | **React Router** | Page navigation |
| Hosting | **Cloudflare Pages** (free) | Unlimited bandwidth, private org repos, global CDN, auto-deploy from GitHub |
### Pages (mapped from current Streamlit tabs)
| Streamlit Tab | React Page | Route | Key Components |
|---------------|------------|-------|----------------|
| Vineyard Advisor | `Advisor.tsx` | `/` | Chatbot, weather card, energy card, shading card, irrigation card |
| System Status | `Control.tsx` | `/control` | Tracker angles, energy budget gauge, control replay, ROI |
| Overview | `Dashboard.tsx` | `/dashboard` | Energy history, crop comparisons |
| Photosynthesis & Data | `Photosynthesis.tsx` | `/photosynthesis` | FvCB/ML results, sensor explorer |
| Forecasting | β | merged into `/dashboard` | Weather + PS forecast cards |
| Shading Simulator | `Shading.tsx` | `/shading` | Interactive offset tester |
| Documentation | `Docs.tsx` | `/docs` | Help, API reference |
### Data fetching pattern
Using TanStack Query for all API calls. Auto-refetch on window focus and
configurable polling intervals.
```typescript
// hooks/useWeather.ts
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
export function useCurrentWeather() {
return useQuery({
queryKey: ["weather", "current"],
queryFn: () => api.get("/api/weather/current"),
refetchInterval: 60_000, // 60s auto-refresh
});
}
```
```typescript
// lib/api.ts
const API_URL = import.meta.env.VITE_API_URL;
export const api = {
get: async (path: string) => {
const res = await fetch(`${API_URL}${path}`);
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
post: async (path: string, body: unknown) => {
const res = await fetch(`${API_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
};
```
### Frontend β Backend communication
| Pattern | Use Case | Protocol |
|---------|----------|----------|
| REST | All data reads, chatbot messages | HTTPS (fetch via TanStack Query) |
| Polling | Dashboard auto-refresh | 60s interval via `refetchInterval` |
| WebSocket | Live tracker angles, energy (future) | WSS (native WebSocket, future) |
---
## Infrastructure & Deployment
### Hosting (all free tier)
| Layer | Platform | Tier | Limits |
|-------|----------|------|--------|
| Frontend | **Cloudflare Pages** | Free | Unlimited bandwidth, 500 builds/month, auto-deploy from GitHub |
| API server | **HuggingFace Spaces** | Free (Docker SDK) | 2 vCPU, 16GB RAM, persistent container, private spaces |
| Cron workers | **GitHub Actions** | Free (private repos) | 2,000 min/month, plenty for 15-min cron + daily planner |
| Cache | **Upstash Redis** | Free | 10K commands/day, 256MB, REST API |
| Secrets | **HF Space secrets** + **Cloudflare env vars** + **GitHub Actions secrets** | Free | Per-service |
| Repo | **GitHub** | Free private | Unlimited |
### Deployment topology
```
GitHub (private repo, main branch)
β
βββββ push βββββΊ Cloudflare Pages (auto-deploy)
β βββ frontend/ β Vite build β static React app
β βββ URL: baseline.pages.dev (or custom domain)
β βββ Env: VITE_API_URL=https://<user>-solarwine-api.hf.space
β
βββββ push βββββΊ HuggingFace Space (auto-deploy via HF GitHub sync)
β βββ backend/ β Dockerfile β FastAPI (uvicorn, port 7860)
β βββ URL: https://<user>-solarwine-api.hf.space
β βββ Secrets: TB creds, IMS token, Gemini key, Redis URL
β
βββββ cron βββββΊ GitHub Actions (scheduled workflows)
βββ control-tick.yml: */15 * * * * β pip install + python control_tick.py
βββ daily-planner.yml: 0 2 * * * (02:00 UTC = 05:00 IST)
βββ Secrets: TB creds, IMS token, Redis URL
Upstash Redis
βββ URL: solarwine-cache-*.upstash.io (shared by API + workers)
```
### GitHub Actions cron workflows
The ControlLoop and DayAheadPlanner run as scheduled GitHub Actions. This
replaces the need for a separate worker host.
```yaml
# .github/workflows/control-tick.yml
name: Control Loop Tick
on:
schedule:
- cron: "*/15 * * * *" # every 15 min
workflow_dispatch: # manual trigger
jobs:
tick:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt -r backend/requirements.txt
- run: python -m backend.workers.control_tick
env:
THINGSBOARD_HOST: ${{ secrets.THINGSBOARD_HOST }}
THINGSBOARD_USERNAME: ${{ secrets.THINGSBOARD_USERNAME }}
THINGSBOARD_PASSWORD: ${{ secrets.THINGSBOARD_PASSWORD }}
IMS_API_TOKEN: ${{ secrets.IMS_API_TOKEN }}
UPSTASH_REDIS_URL: ${{ secrets.UPSTASH_REDIS_URL }}
UPSTASH_REDIS_TOKEN: ${{ secrets.UPSTASH_REDIS_TOKEN }}
```
```yaml
# .github/workflows/daily-planner.yml
name: Day-Ahead Planner
on:
schedule:
- cron: "0 2 * * *" # 02:00 UTC = 05:00 IST
workflow_dispatch:
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt -r backend/requirements.txt
- run: python -m backend.workers.daily_planner
env:
THINGSBOARD_HOST: ${{ secrets.THINGSBOARD_HOST }}
THINGSBOARD_USERNAME: ${{ secrets.THINGSBOARD_USERNAME }}
THINGSBOARD_PASSWORD: ${{ secrets.THINGSBOARD_PASSWORD }}
IMS_API_TOKEN: ${{ secrets.IMS_API_TOKEN }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
UPSTASH_REDIS_URL: ${{ secrets.UPSTASH_REDIS_URL }}
UPSTASH_REDIS_TOKEN: ${{ secrets.UPSTASH_REDIS_TOKEN }}
```
### Docker setup (HuggingFace Space)
```dockerfile
# backend/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
COPY backend/requirements.txt backend/
RUN pip install -r requirements.txt -r backend/requirements.txt
COPY src/ src/
COPY config/ config/
COPY backend/ backend/
COPY Data/ Data/
ENV PYTHONPATH=/app
# HuggingFace Spaces requires port 7860
EXPOSE 7860
CMD ["uvicorn", "backend.api.main:app", "--host", "0.0.0.0", "--port", "7860"]
```
### Environment variables
| Variable | Where | Source |
|----------|-------|--------|
| `THINGSBOARD_HOST` | HF Space, GH Actions | HF secrets, GH secrets |
| `THINGSBOARD_USERNAME` | HF Space, GH Actions | HF secrets, GH secrets |
| `THINGSBOARD_PASSWORD` | HF Space, GH Actions | HF secrets, GH secrets |
| `IMS_API_TOKEN` | HF Space, GH Actions | HF secrets, GH secrets |
| `GOOGLE_API_KEY` | HF Space, GH Actions | HF secrets, GH secrets |
| `UPSTASH_REDIS_URL` | HF Space, GH Actions | HF secrets, GH secrets |
| `UPSTASH_REDIS_TOKEN` | HF Space, GH Actions | HF secrets, GH secrets |
| `JWT_SECRET` | HF Space | HF secrets |
| `ALLOWED_ORIGINS` | HF Space | HF secrets (Cloudflare Pages URL) |
| `VITE_API_URL` | Cloudflare Pages | Cloudflare env vars (HF Space URL) |
---
## Data Flow
### Request flow (user views dashboard)
```
Browser β Cloudflare CDN β React SPA (static)
β
βββ useQuery β fetch https://<user>-solarwine-api.hf.space/api/weather/current
β
βΌ
FastAPI route (HuggingFace Spaces)
β
βΌ
DataHub.weather.get_current()
β
βββ Redis cache hit? β return cached
βββ Redis miss β IMS API β cache β return
```
### Control flow (15-min tick)
```
GitHub Actions cron (*/15 * * * *)
β
βββ python -m backend.workers.control_tick
β
βΌ
ControlLoop.tick()
β
βββ DataHub.weather.get_current() β IMS (via Redis cache)
βββ DataHub.vine_sensors.get_snapshot() β ThingsBoard
βββ RoutingAgent β FvCB or ML model
βββ DayAheadPlan lookup β Redis
βββ InterventionGate check
βββ BudgetGuard check
βββ CommandArbiter β decide angle
βββ TrackerDispatcher β ThingsBoard shared attributes
βββ EnergyBudget.spend_slot()
βββ Save TickResult β Redis (for API /control/status)
```
### Chatbot flow
```
Browser β React β POST /api/chatbot/message { message, session_id }
β
βΌ
FastAPI route (HF Space) β VineyardChatbot.chat(message, hub=DataHub)
β
βββ QueryClassifier β data / knowledge / greeting
βββ Gemini Pass 1 β tool calls (if data query)
βββ DataHub.dispatch_tool() β real data
βββ tag_tool_result() β source metadata
βββ Gemini Pass 2 β grounded response
βββ ResponseValidator β rule checks
βββ estimate_confidence()
βββ Return ChatResponse β JSON β Browser
```
---
## Security
| Concern | Mitigation |
|---------|-----------|
| API keys in repo | `.env` in `.gitignore`; secrets in HF/Cloudflare/GH settings |
| TB credentials | Server-side only; frontend never sees TB creds |
| Gemini API abuse | Rate limit 10 req/min on `/api/chatbot/message` |
| CORS | Whitelist Cloudflare Pages domain only |
| Auth (Phase 11.6) | JWT tokens; initially optional for research use |
| Tracker commands | Only GH Actions worker can write to TB; API is read-only |
| HF Space | Set to **private** β not publicly accessible without auth |
---
## Migration from Streamlit
The existing `app.py` and `ui/` folder remain functional throughout migration.
They are deprecated once the Lovable frontend reaches feature parity.
| Phase | Streamlit | Lovable/React | Notes |
|-------|-----------|---------------|-------|
| 11.1β11.3 | Primary | Not started | Backend being built |
| 11.4 | Primary | Scaffold | Pages generated via Lovable |
| 11.5 | Deprecated | Primary | Feature parity reached |
| 11.6+ | Removed | Primary | Auth, monitoring, polish |
---
## Scaling path (when free tier is outgrown)
| Bottleneck | Upgrade | Cost |
|-----------|---------|------|
| HF Space resources | HF Space upgrade (2Γ CPU, 16GB) | $0 (already free) β $9/mo for GPU |
| GitHub Actions minutes | GitHub Team (3,000 min) | $4/user/mo |
| Upstash 10K commands/day | Upstash Pay-as-you-go ($0.20/100K) | ~$1/mo |
| Cloudflare Pages | Already unlimited on free tier | $0 |
| Heavy ML inference | Add GPU worker (Modal/RunPod) | $5β10/mo |
| Multi-site (>1 vineyard) | Add Postgres (Neon free) for multi-tenant state | Free β $19/mo |
Total cost to run a real commercial deployment: **~$25/month**.
|