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