api / context /4_production.md
safraeli's picture
Deploy: 2026 sensor migration + redesign + bucket B endpoints
13fc29d verified
|
Raw History Blame Contribute Delete
22.5 kB

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:

---
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.

// 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
  });
}
// 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.

# .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 }}
# .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)

# 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.