api / context /3_todo.md
safraeli's picture
Deploy: 2026 sensor migration + redesign + bucket B endpoints
13fc29d verified
|
Raw History Blame Contribute Delete
61.8 kB
# SolarWine – Todo Tracker
This file mirrors the high-level plan in `2_plan.md` and tracks day-to-day progress.
---
## Baseline v1 (Complete)
- [x] **scaffold** – project directory structure, `.env`, `.gitignore`, `requirements.txt`
- [x] **context-files** – create `context/1_purpose.md`, `context/2_plan.md`, `context/3_todo.md`
- [x] **git-init** – create git repository and make initial commit
- [x] **oop-architecture** – implement OOP modules in `src/`
- [x] **ims-fetch** – implement `IMSClient` and cache IMS data
- [x] **stage1-farquhar** – implement mechanistic Farquhar/Greer–Weedon model
- [x] **stage1-validate** – validate Stage 1 A outputs and plots
- [x] **stage2-preprocess** – preprocessing and temporal split
- [x] **stage2-models** – train and evaluate ML models
- [x] **stage2-visualize** – build evaluation visualizations
- [x] **streamlit-app** – build Streamlit UI (6 tabs)
---
## SolarWine 2.0
### Phase 1: Configuration & Data Infrastructure
- [x] **config-extend** – add energy budget, no-shade windows, fruiting zone, and tradeoff params to `config/settings.py` (most params added in earlier session; no-shade windows `NO_SHADE_BEFORE_HOUR`, `NO_SHADE_MONTHS`, `NO_SHADE_GHI_BELOW`, `NO_SHADE_TLEAF_BELOW` and `SHADE_ELIGIBLE_HOURS` added 2026-03-10; guardrails wired to reference settings)
- [x] **data-schema** – `src/data/data_schema.py` (telemetry dataclasses: SensorRaw, BiologicalState, TrackerKinematics, SimulationLog). Re-exported via `src/data_schema.py`
- [x] **import-layout** – `scripts/import_layout.py` generates `Data/layout.json` from config geometry + TB device registry (4 rows, 22 devices with 3D positions). Added 2026-03-10
### Phase 2: Biological Digital Twin Upgrades
- [x] **fvcb-semillon** – upgrade `src/farquhar_model.py` with `calc_photosynthesis_semillon()` β†’ returns (A, limiting_state, shading_helps). Added 2026-03-10: uses configurable `transition_temp` (default `SEMILLON_TRANSITION_TEMP_C=30Β°C`); `shading_helps` is True only when Rubisco-limited AND Aj > Ac (light abundant relative to enzyme capacity)
- [x] **shadow-fruiting** – upgrade `src/solar_geometry.py` with `fruiting_zone_shadow()` and `evaluate_candidate_offsets()` (added 2026-03-10)
- [x] **canopy-expose** – `compute_vine_A()` now returns `fruiting_zone_A`, `fruiting_zone_par`, `top_canopy_A`, `top_canopy_par` alongside existing fields. Added 2026-03-10
- [x] **safety-rails** – `src/advisor/safety_rails.py` (FvCB vs ML divergence check, 12% threshold β†’ fallback). Re-exported via `src/safety_rails.py`
- [x] **spectral-agg** – `src/spectral_aggregator.py` with `aggregate_spectral()` (single-timestep) and `aggregate_spectral_df()` (batch DataFrame). CWSI from explicit value β†’ delta-T proxy β†’ VPD fallback. Physical bounds clipping with quality flags. Added 2026-03-10
### Phase 3: The Control Algorithm (Core)
- [x] **energy-budget** – create `src/energy_budget.py` (hierarchical Yearβ†’Monthβ†’Weekβ†’Dayβ†’Slot budget planner). Added 2026-03-10: `EnergyBudgetPlanner` with `compute_annual_plan()`, `compute_weekly_plan()`, `compute_daily_plan()`, `spend_slot()`, `emergency_draw()`, `compute_daily_rollover()`. Analytical fallback when no ShadowModel. Budget: ~920 kWh potential β†’ 46 kWh budget (5%), July gets 17.6 kWh (45%)
- [x] **intervention-gate** – `InterventionGate` in `src/shading/tradeoff_engine.py` (physiology-only pass/fail: GHI, Tleaf, CWSI, shading_helps; no time/month hard-coding β€” geometry decides). Re-exported via `src/tradeoff_engine.py`
- [x] **min-dose-engine** – `TradeoffEngine` in `src/shading/tradeoff_engine.py` (smallest offset satisfying: fruiting PAR < 400 Β΅mol, top canopy β‰₯ 85% baseline, sacrifice ≀ budget; geometric feasibility pre-check; face-PAR sun-side selection)
- [x] **command-arbiter** – `src/command_arbiter.py` `CommandArbiter` (5-level priority stack: weatherβ†’harvestβ†’safetyβ†’timeoutβ†’engine; hysteresis filter with configurable window/tolerance; `arbitrate()` combines selection + filter; weather/harvest bypass hysteresis). Added 2026-03-10
- [x] **astro-tracker** – `AstronomicalTracker` in `src/command_arbiter.py` (pure sun-following via ShadowModel; `get_angle(timestamp)` interface). Added 2026-03-10
### Phase 3.5: Agronomic Weighting & Day-Ahead Planner
- [x] **zone-crop-weights** – spatial zone weighting implemented in `DayAheadPlanner._get_crop_multiplier()` using `STAGE_CROP_MULTIPLIER` + `ZONE_CROP_WEIGHTS` from settings. Added 2026-03-10
- [x] **day-ahead-dp** – created `src/day_ahead_planner.py` with `DayAheadPlanner` (DP trajectory optimizer: forward pass over daylight slots Γ— candidate offsets, budget constraint via discretised DP, movement cost penalty for smooth trajectories, simplified InterventionGate using forecast data). Returns `DayAheadPlan` with `SlotPlan` list + explainability tags. Added 2026-03-10
- [x] **daily-plan-output** – `DayAheadPlan.to_dict()` serialises plan to JSON-ready dict with per-slot offset, energy cost, gate status, and tags. Added 2026-03-10
- [x] **phenology-tracker** – `src/models/phenology.py` upgraded with three estimation methods: (1) GDD-based via `compute_gdd_from_ims()` + `estimate_stage_by_gdd()` using IMS tdmax/tdmin and `PHENOLOGY_GDD_THRESHOLDS`, (2) camera-based via `detect_stage_from_camera()` using Gemini Vision on live vineyard feed, (3) calendar fallback. `estimate_stage_combined()` merges all three (camera high-confidence > GDD > calendar). Backward-compatible API. Added 2026-03-10
- [x] **baseline-ps** – created `src/baseline_predictor.py` (hybrid FvCB+ML with RoutingAgent rule-based selection per slot); wired into DayAheadPlanner as optional `baseline_predictor` param, replaces temperature heuristic with actual A predictions for crop value
- [x] **arbiter-plan-integration** – ControlLoop now tracks plan divergence (cumulative kWh + consecutive slots), triggers re-plan when thresholds exceeded, with cooldown to avoid thrashing
- [x] **tracker-rpc** – added `send_rpc_command()` and `set_device_attributes()` to ThingsBoardClient (two-way RPC with one-way fallback, shared attribute writes)
- [x] **tracker-verify** – TrackerDispatcher verifies |actual - target| < tolerance after dispatch via `_verify_all()`
- [x] **tracker-dispatch** – created `src/tracker_dispatcher.py`: takes `ArbiterDecision` β†’ sends to all 4 trackers via RPC/attribute fallback β†’ verifies execution β†’ logs result
- [x] **control-loop** – created `src/control_loop.py`: 15-min main loop (fetch sensors β†’ plan lookup β†’ live gate check β†’ budget guard β†’ arbitrate β†’ fleet overrides β†’ dispatch β†’ budget spend β†’ divergence check β†’ re-plan β†’ log). One-shot, continuous, and dry_run modes. TrackerFleet/Scheduler wired for per-tracker overrides (default = all same). EnergyBudgetPlanner wired for real-time budget consumption. Plan divergence tracking with auto re-plan
### Phase 4: Operational Modes
- [x] **ops-modes** – created `src/operational_modes.py` (WindStow, HailStow, HeatShield, HarvestMode + composite `OperationalModeChecker`; integrated into ControlLoop)
### Phase 5: ROI Reporting
- [x] **roi-service** – created `src/roi_service.py` (BudgetStatus, InterventionStats, LERResult, ROIService with budget tracking + LER computation)
### Phase 6: Dashboard v2
- [x] **tab-system-status** – unified System Status tab (`ui/tab_system_status.py`) with 5 sub-sections:
- **Tracker Status**: live angles from TB, coherence check, bar chart
- **Energy Budget**: annual/monthly budget allocation, gauge, weekly plan
- **Control Replay**: load simulation logs, timeline charts, slot-by-slot detail
- **Fruiting Zone**: live canopy temp/PAR/VPD, sunburn risk assessment, thresholds
- **ROI Dashboard**: budget utilisation gauge, intervention stats, LER calculator
### Phase 7: Integration Testing
- [x] **sim-script** – created `scripts/run_control_simulation.py` (historical replay through full control loop with per-day summary, CLI args, JSON output)
- [x] **validate-budget** – `scripts/validate_control_system.py` verifies cumulative sacrifice never exceeds `max_energy_reduction_pct` (tested with real sim log + synthetic violations)
- [x] **validate-gates** – validates no interventions in May, before 10:00, below transition temp, or below GHI threshold (3/3 violation types caught in self-test)
- [x] **validate-dose** – validates average offset < 10Β°, max offset < 20Β°, intervention rate < 30% (catches excessive dose in self-test)
### Phase 8: AI-Enhanced Forecasting & Routing
#### 8A β€” Chronos-2 Multivariate Day-Ahead Forecasting
- [x] **install-chronos** – install `chronos-forecasting>=2.0` and verify compatibility with existing env (done: conda env `solarwine` with Python 3.12, chronos 2.2.2, torch 2.10, llvm-openmp for XGBoost)
- [x] **chronos-data-prep** – build multivariate input pipeline: combine historical A_n (target) with past IMS station 43 measurements + on-site sensors (PAR, T_canopy, VPD, CWSI) as historical context, plus IMS *forecast* data (predicted GHI, air temperature, wind speed, humidity) as known-future covariates for the prediction horizon; IMS forecasts are available at inference time so this is not a data leak β€” only actual future sensor measurements would be
- [x] **chronos-predict** – execute zero-shot 96-step (24h @ 15-min) day-ahead A_n prediction using Chronos-2 (`amazon/chronos-t5-small` or `-base`) with multivariate context (past A_n + past sensor measurements as history, IMS forecasts as known-future covariates); integrate into `src/day_ahead_planner.py` baseline prediction
- [x] **chronos-benchmark** – benchmark Chronos-2 MAE against existing ML baseline (MAE β‰ˆ 2.7) on held-out test set; compare univariate (A_n only) vs multivariate (A_n + IMS covariates) performance; document results and decide on integration path
#### 8B β€” Gemini LLM Data Engineering
- [x] **install-gemini** – `google-genai>=1.0` added to `requirements.txt`; already installed in `solarwine` conda env (v1.65.0)
- [x] **llm-data-cleaning** – `src/llm_data_engineer.py` `LLMDataEngineer.analyze_anomalies()`: sends per-column descriptive stats + domain context (physical ranges, Negev site notes) to Gemini; receives Z-score/IQR/bound thresholds as JSON; `apply_cleaning()` applies clip/nan/drop strategies; graceful fallback to physics-based bounds when API key unavailable
- [x] **llm-feature-eng** – `LLMDataEngineer.engineer_features()`: adds `hour_sin`, `hour_cos`, `doy_sin`, `doy_cos` (cyclical time encodings) and `stress_risk_score` (normalised weighted VPD + CWSI, weights confirmed by Gemini); `run_pipeline()` orchestrates clean β†’ feature-engineer in one call
- [x] **dashboard-8b** – Data Explorer: added **AI Data Engineering** as third radio option (Gemini anomaly thresholds, cleaning summary, before/after charts, engineered features, daytime stress profile). Presentation tab: added Gemini LLM data engineering to β€œWhat’s Next” completed list, Key takeaways, and β€œWhat each tab shows” (Data Explorer bullet). All data precomputed in-tab (no cache loader).
#### 8C β€” Gemini Intelligent Routing Layer
- [x] **routing-prompt** – define supervisory system prompt for Gemini model router: given real-time telemetry (Temp, Irradiance, CWSI), route to FvCB (Model A, accurate under standard conditions) or ML (Model B, handles non-linear stress) β€” reply only `MODEL_A` or `MODEL_B`
- [x] **routing-agent** – build `RoutingAgent` function: executes Gemini API call at start of 15-min control loop, constructs prompt from live telemetry, parses response to determine model path
- [x] **arbiter-routing** – RoutingAgent wired into ControlLoop tick (step 2b): routes each slot to FvCB or ML via rule-based fast path (>90% of cases) with Gemini fallback for transition zone; `model_route` field added to TickResult; safety_rails divergence check remains as independent safety layer
#### 8E β€” Gemini Day-Ahead Advisor
- [x] **day-ahead-advisor** – `src/day_ahead_advisor.py` DayAheadAdvisor: Gemini analyzes IMS forecast + vine state β†’ structured stress profile, budget recommendations, model routing, Chronos sanity check; dashboard display in Predictions tab
#### 8D β€” Shadow Mode Validation
- [x] **shadow-mode-test** – created `scripts/shadow_mode_test.py`: 4 validation tests β€” (1) LLM data cleaning filters catch anomalies, (2) BaselinePredictor generates valid A profiles (peak=17.0 Β΅mol, daylight mean=6.1), (3) RoutingAgent routes coolβ†’fvcb and hotβ†’ml correctly, (4) full pipeline shadow simulation over date range
### Phase 9: Vineyard Advisor Chatbot
- [x] **chatbot-todo** – add Phase 9 to `context/3_todo.md`
- [x] **chatbot-scaffold** – create `src/vineyard_chatbot.py` scaffold + Gemini client (lazy init, same pattern as DayAheadAdvisor)
- [x] **chatbot-system-prompt** – write `CHATBOT_SYSTEM_PROMPT` (persona, site facts, 8 biology rules, tool definitions, response rules)
- [x] **chatbot-tool-defs** – implement `TOOL_DEFINITIONS` list + `_dispatch_tool()` router
- [x] **chatbot-tools-weather** – implement `_tool_current_weather()`, `_tool_calc_photosynthesis()`, `_tool_explain_rule()`
- [x] **chatbot-tools-shading** – implement `_tool_simulate_shading()`, `_tool_compare_angles()`, `_tool_daily_schedule()`, `_tool_day_ahead_advisory()`
- [x] **chatbot-chat-flow** – implement two-pass chat flow (`_build_messages()`, `_call_gemini()`, `chat()`)
- [x] **chatbot-tab** – add Vineyard Advisor tab to `app.py` (Vineyard Advisor tab, chat UI, session state)
- [x] **chatbot-quick-actions** – add quick-action buttons + `_render_tool_data()` helper + clear chat
- [x] **chatbot-test** – syntax check, import check, tabs render, tool call test, fallback test
### Phase 9.5: Chatbot Data Integration
Wire all five data sources into the chatbot (IMS weather, TB sensors, PS prediction,
energy generation, energy prediction) via a loosely-coupled provider layer.
#### Architecture (`src/data_providers.py`)
```
VineyardChatbot ──▢ DataHub ──┬── WeatherService (IMS, 30-min TTL cache)
(tool dispatch) (registry) β”œβ”€β”€ VineSensorService (TB, 5-min TTL snapshot)
β”œβ”€β”€ PhotosynthesisService (FvCB + ML + forecast)
β”œβ”€β”€ EnergyService (TB generation + prediction)
β”œβ”€β”€ AdvisoryService (Gemini day-ahead)
└── BiologyService (rules lookup)
```
- Chatbot never imports data clients directly β€” only talks to `self.hub`.
- Each Service owns its caching (TTLCache), error handling, lazy init.
- Adding a new source = write a Service subclass + `hub.register()`.
- `summarise_dataframe()` auto-compresses large results for LLM context.
#### Data strategy
| Source | Service | Strategy | Detail |
|---|---|---|---|
| IMS weather | WeatherService | Cache (preload) | `ims_merged_15min.csv`, 30-min TTL |
| TB sensor snapshot | VineSensorService | On-demand + 5-min TTL | `get_vine_snapshot()` |
| TB sensor time-series | VineSensorService | On-demand | `get_timeseries()` β†’ hourly downsample |
| PS prediction (FvCB/ML) | PhotosynthesisService | On-demand (compute) | Lazy-load models |
| Energy generation (TB) | EnergyService | On-demand | Fetch from inverter/meter devices |
| Energy prediction | EnergyService | On-demand (compute) | GHI Γ— panel Γ— efficiency Γ— cos(ΞΈ) |
#### Steps
- [x] **chatbot-provider-scaffold** – Create `src/data_providers.py` with BaseService ABC, TTLCache, summarise_dataframe, DataHub registry, and 6 service classes (Weather, VineSensor, Photosynthesis, Energy, Advisory, Biology)
- [x] **chatbot-hub-wiring** – Refactored `VineyardChatbot.__init__` to accept a `DataHub` (default: `DataHub.default()`); rewrote `_dispatch_tool()` to delegate to hub services (15 tools); removed all direct imports of IMSClient/ThingsBoardClient/FarquharModel from chatbot
- [x] **chatbot-ims-history** – Implemented `WeatherService.get_history()` with hourly resample + `summarise_dataframe()`; added `get_weather_history` tool to system prompt and dispatch
- [x] **chatbot-tb-timeseries** – Implemented `VineSensorService.get_history()` (device_type, area, hours_back β†’ hourly averages via `get_timeseries()`); added `get_sensor_history` tool to system prompt and dispatch
- [x] **chatbot-ml-predictor** – Implemented `PhotosynthesisService.predict_ml()` with train-once-cache pattern: lazy loads sensor data + IMS, runs Preprocessor + PhotosynthesisPredictor, picks best model by MAE, auto-fills features from latest IMS if not provided. *Note: requires IMS cache with temperature/GHI overlapping sensor data dates*
- [x] **chatbot-ps-forecast** – Implemented `PhotosynthesisService.forecast_day_ahead()`: FvCB-based projection over IMS hourly weather (PAR~2Γ—GHI, Tleaf~Tair+2Β°C, VPD from T+RH). Returns hourly A profile + peak/stress summary
- [x] **chatbot-energy-devices** – Discovered Plant asset "Yeruham Vineyard" (ASSET, not DEVICE) with `power` (W) + `production` (Wh/5min) keys. Added `ASSET_REGISTRY`, `get_asset_timeseries()`, `get_asset_latest()` to TB client. Also registered 4 Tracker devices (501-509) with angle/mode telemetry
- [x] **chatbot-energy-tool** – `EnergyService` rewritten: past/today β†’ real TB Plant asset data (daily ~270-290 kWh for 48 kW system); future β†’ analytical estimate (persistence forecast Γ— system capacity). Hourly profiles in Israel local time
- [x] **chatbot-energy-predict** – `EnergyService.predict()` now routes: past/today β†’ `get_daily_production()` (real TB), future β†’ `_predict_analytical()` (IMS GHI Γ— 48 kW)
- [x] **chatbot-prompt-update** – Updated system prompt with all 15 tools (grouped: weather, vine sensors, photosynthesis, shading, energy, advisory, biology); added energy/prediction/irrigation/fertiliser keywords to fallback matcher
- [x] **chatbot-integration-test** – All 15 tools dispatch correctly; verified imports + instantiation (no circular imports); tested fallback keyword matching; `summarise_dataframe()` compresses >48-row results. Remaining data gaps: IMS cache lacks GHI/temperature (only wind_speed_ms), no date overlap between IMS and sensors for ML training
### Phase 10: Reliable Advisor β€” Anti-Hallucination Guardrails
Deterministic guardrails to prevent the chatbot from hallucinating data,
violating biology rules, or answering data questions without tool grounding.
#### Step 1: Structured Response Schema
- [x] **structured-response** – Extended `ChatResponse` with `confidence`, `sources`, `caveats`, `rule_violations` fields; all responses carry grounding metadata
#### Step 2: Mandatory Tool Grounding
- [x] **query-classifier** – `classify_query()` in `src/chatbot/guardrails.py`: regex-based classification (data / knowledge / greeting / ambiguous); data queries force tool calls
- [x] **tool-enforcement** – If LLM answers a data query without calling a tool, chatbot re-prompts: "You MUST call a tool"
#### Step 3: Post-Response Rule Validator
- [x] **response-validator** – `validate_response()` in `src/chatbot/guardrails.py`: deterministic checks for no_shade_before_10 (block), no_shade_in_may (block), temperature_transition (warn), no_leaves_no_shade_problem (warn), no_shading_must_explain (warn)
- [x] **validator-integration** – Validator runs on every response in `chat()`; block-severity overrides response text; warn-severity appends caveats
#### Step 4: Source-Tagged Context Injection
- [x] **source-tagging** – `tag_tool_result()` adds `_source`, `_data_age_minutes`, `_freshness_warning` to tool results before Gemini Pass 2
- [x] **citation-prompt** – Pass 2 prompt instructs Gemini to cite source and timestamp when quoting numbers
#### Step 5: RAG Knowledge Base
- [x] **rag-knowledge-base** – Keyword-indexed rule retrieval with pinned rules (no_shade_before_10, energy_budget, temperature_transition) + top-scoring rules per query; `retrieve_relevant_rules()` + `build_contextual_prompt()` in vineyard_chatbot.py
#### Step 6: Conversation Memory
- [x] **sliding-context** – Summarize older messages into topics, keep recent 6 verbatim, pin critical context; `_summarize_history()` in vineyard_chatbot.py
#### Step 7: Confidence-Gated Responses
- [x] **confidence-estimator** – `estimate_confidence()` maps data freshness to high/medium/low/insufficient_data
- [x] **confidence-ui** – `_render_grounding_metadata()` in `ui/tab_advisor.py` shows colored confidence badge, sources, caveats, rule warnings
#### Step 8: Guardrail Test Suite
- [x] **qa-tests-5** – 5 adversarial QA tests in `tests/test_advisor_chatbot.py`: shade-before-10 blocked, data-question forces tool, stale-data warns, no-shade-without-reason warns, tool-failure returns insufficient_data
- [x] **guardrail-unit-tests** – Unit tests for QueryClassifier, ResponseValidator, ConfidenceEstimation, SourceTagging (42 tests total, all passing)
- [x] **qa-tests-expand** – Expanded to 21 adversarial prompts (QA1-QA21) covering May shading, dormancy, night queries, prompt injection, boundary conditions, multi-rule violations, energy/irrigation classification, ambiguous queries. 58 tests total, all passing
#### Step 9: Dual-Channel Advisory
- [x] **dual-channel** – `classify_response_mode()` separates info mode (blue, factual) from advisory mode (amber, recommendations); `response_mode` field on ChatResponse; UI renders mode indicator
#### Step 10: Feedback Loop
- [x] **feedback-ui** – Thumbs up/down/flag buttons on each response in `ui/tab_advisor.py`; `_render_feedback_buttons()` + `_submit_feedback()`
- [x] **feedback-storage** – `src/chatbot/feedback.py` logs feedback to JSONL (query, response, tool_calls, rules, confidence, comment)
---
## Phase 11: Production Migration
Full architecture documented in `context/4_production.md`.
3-layer architecture: Lovable/React frontend (Cloudflare Pages) β†’ FastAPI middleware (HuggingFace Spaces) β†’ existing Python core.
Cron workers via GitHub Actions (free for private repos).
### Phase 11.1: Redis Caching Layer
Replace in-memory TTLCache with Upstash Redis so API and workers share state.
- [x] **upstash-setup** – Upstash Redis instance created (many-mammal-76248.upstash.io), credentials in `.env`, connectivity verified with dual-layer TTLCache round-trip
- [x] **redis-client** – Create `src/data/redis_cache.py`: thin wrapper around Upstash Redis REST API with `get_json()`, `set_json(ttl)`, `delete()`, `exists()`, `ping()`. Singleton via `get_redis()`, returns None when not configured
- [x] **datahub-redis** – Refactored `TTLCache` in `data_providers.py`: tries Redis first (via `redis_prefix`), falls back to in-memory. WeatherService prefix `weather:`, VineSensorService prefix `vine:`. Streamlit works unchanged (no Redis = pure in-memory)
- [x] **tick-result-redis** – `backend/workers/control_tick.py` writes TickResult to Redis `control:last_tick` (20-min TTL) after each tick
- [x] **plan-redis** – `backend/workers/daily_planner.py` writes plan to Redis `control:plan` (24h TTL) + backup `Data/daily_plan.json`
- [x] **budget-redis** – ControlLoop `_ensure_daily_budget()` restores from Redis `control:budget` on startup; `_persist_budget()` saves after every `spend_slot()`. Falls back to recompute if Redis unavailable
### Phase 11.2: FastAPI Middleware (HuggingFace Spaces)
Create the API gateway in `backend/api/`, deploy as HF Space (Docker SDK).
- [x] **fastapi-scaffold** – Created `backend/api/main.py` with FastAPI app, CORS (configurable via `ALLOWED_ORIGINS`), lifespan, all route includes
- [x] **fastapi-deps** – Created `backend/api/deps.py` with `get_datahub()` (lru_cache singleton) and `get_redis_client()`
- [x] **route-weather** – `backend/api/routes/weather.py`: GET `/api/weather/current`, `/api/weather/history?start_date&end_date`, `/api/weather/forecast`
- [x] **route-sensors** – `backend/api/routes/sensors.py`: GET `/api/sensors/snapshot`, `/api/sensors/history?type&area&hours`
- [x] **route-energy** – `backend/api/routes/energy.py`: GET `/api/energy/current`, `/api/energy/daily/{date}`, `/api/energy/history`, `/api/energy/predict/{date}`
- [x] **route-photosynthesis** – `backend/api/routes/photosynthesis.py`: GET `/api/photosynthesis/current?model`, `/api/photosynthesis/forecast`
- [x] **route-control** – `backend/api/routes/control.py`: GET `/api/control/status`, `/api/control/plan`, `/api/control/budget`, `/api/control/trackers`
- [x] **route-chatbot** – `backend/api/routes/chatbot.py`: POST `/api/chatbot/message`, POST `/api/chatbot/feedback` with Pydantic request models
- [x] **route-biology** – `backend/api/routes/biology.py`: GET `/api/biology/phenology`, `/api/biology/rules`, `/api/biology/rules/{name}`
- [x] **rate-limiting** – Added `slowapi`: 60 req/min global default, 10 req/min on `/api/chatbot/message`
- [x] **fastapi-dockerfile** – Created `backend/Dockerfile` (Python 3.12 slim, port 7860, copies src/config/backend/Data)
- [x] **hf-space-readme** – Created `backend/HF_README.md` (sdk: docker, app_port: 7860, private: true, endpoint summary)
- [x] **fastapi-test** – Smoke tested: `/api/health` returns OK (uptime + redis status), `/api/weather/current` returns live IMS data
### Phase 11.3: Background Workers (GitHub Actions)
Extract ControlLoop and DayAheadPlanner into standalone entry points, run as scheduled GitHub Actions.
- [x] **worker-control** – Created `backend/workers/control_tick.py`: loads ControlLoop, runs single tick, writes TickResult to Redis, supports `--dry-run`
- [x] **worker-planner** – Created `backend/workers/daily_planner.py`: runs DayAheadPlanner, writes plan to Redis + JSON backup
- [x] **gh-action-tick** – Created `.github/workflows/control-tick.yml`: schedule `*/15 * * * *`, Python 3.12 + pip cache, all secrets
- [x] **gh-action-planner** – Created `.github/workflows/daily-planner.yml`: schedule `0 2 * * *`, Python 3.12 + pip cache, all secrets
- [x] **gh-secrets** – All 7 secrets added to GitHub repo Settings β†’ Actions secrets
- [x] **worker-logging** – Structured JSON logging (asctime + level + name + message) in both workers
- [x] **worker-test** – Dry-run tested: `python -m backend.workers.control_tick --dry-run` loads plan (43 slots), computes angle (-10.5Β°), dispatches to 4 trackers (DRY RUN), logs tick result
### Phase 11.4: Lovable Frontend (React + Cloudflare Pages)
Generate and customize the production frontend via Lovable.
- [x] **lovable-project** – Created via Lovable, exported to `solarwine-ai/solarvine-dashboard` repo
- [x] **lovable-dashboard** – Dashboard page: weather card, energy (yesterday/today/tomorrow), phenology, quick stats
- [x] **lovable-advisor** – Advisor page: chat panel with bubbles, advisory cards, confidence/channel badges, feedback
- [x] **lovable-control** – Control page: tracker bars, energy budget, last tick, plan timeline
- [x] **lovable-photosynthesis** – Photosynthesis page: FvCB vs ML, forecast chart, sensor snapshot
- [x] **lovable-shading** – Shading page: offset slider, 3-zone canopy, stress heatmap
- [x] **lovable-docs** – Docs page: biology rules accordion, architecture cards, API reference (18 endpoints)
- [x] **lovable-export** – Imported into `frontend/` directory in main repo
- [x] **api-hooks** – 7 hooks (weather, energy, sensors/PS, control, chatbot, auth) + `api.ts` + `types/api.ts`
- [x] **api-integration** – All 6 pages wired to real API: mock data replaced with live TanStack Query hooks, field names mapped to backend snake_case responses
- [x] **chatbot-component** – Real mutations (useSendMessage/useSendFeedback), confidence badges, source citations, caveats, dual-channel (INFO/ADVISORY), auto-scroll
- [x] **responsive-polish** – Sidebar hamburger already implemented (lg breakpoint). Fixed: Power chart responsive height, Shading heatmap mobile min-width, Home grid breakpoints, camera/map sizing
### Phase 11.5: Deployment & Integration
Wire everything together and deploy.
- [x] **frontend-hosting** – Switched from Vercel (requires Pro for private org repos) to **Cloudflare Pages** (free, unlimited bandwidth, private org repos supported). Connect GitHub repo β†’ root `frontend/`, build `npm run build`, output `dist`, set `VITE_API_URL` env var
- [x] **hf-deploy** – HF Space `SolarWine/api` deployed (Docker SDK, public), secrets injected via huggingface_hub, `/api/health` returns OK with Redis connected. 14/17 endpoints passing; 3 fixed (phenology missing arg, photosynthesis missing method, sensors null area)
- [ ] **upstash-connect** – Verify Redis connectivity from HF Space and GitHub Actions, test cache read/write round-trip
- [x] **e2e-smoke** – Created `scripts/smoke_test.py`: tests 12 endpoints (health, weather, sensors, energy, PS, control, biology, chatbot). Run: `python scripts/smoke_test.py [url]`
- [x] **streamlit-deprecate** – Added migration banner to `app.py`; shows only when `SOLARWINE_FRONTEND_URL` env var is set (invisible until new frontend is live)
- [ ] **dns-custom** – (Optional) Configure custom domain on Cloudflare Pages if available
### Phase 11.6: Security & Monitoring (Post-Launch)
- [x] **auth-jwt** – Created `backend/api/auth.py` (create_token, require_auth, optional_auth) + `backend/api/routes/login.py` (POST `/api/auth/login`). Auth returns `role: "guest"` when JWT_SECRET not set (read-only). PyJWT added to backend/requirements.txt
- [x] **api-hardening** – Standardized error handling (HTTPException everywhere, no JSONResponse for errors), data provider error dict detection, startup path validation, Sentry fail-loud, chatbot session_id validation + blank message guard + init failure circuit breaker, Redis singleton retry-storm fix, CORS defaults include HF Space + Cloudflare Pages URLs
- [x] **auth-frontend** – Created `useAuth.ts` hook (login/logout/token restore from localStorage) + updated `api.ts` to auto-attach JWT Bearer header + auto-clear on 401
- [ ] **monitoring-health** – UptimeRobot or similar free monitor on HF Space `/api/health` (5-min interval, email alerts)
- [x] **monitoring-logs** – Structured logging (asctime/level/name/message) in `main.py` + request logging middleware (method, path, status, duration). Health endpoint extended with ThingsBoard/IMS/Gemini connectivity checks
- [x] **error-tracking** – Added Sentry integration to `backend/api/main.py` (optional, enabled via `SENTRY_DSN` env var). `sentry-sdk[fastapi]>=2.0` in backend/requirements.txt
- [x] **budget-alerts** – `control_tick.py` checks budget after each tick; logs warning if >80% spent before 14:00 IST; sends webhook to `BUDGET_ALERT_WEBHOOK` if configured
---
## Phase 12: Robustness & Efficiency
### 12.1: Immediate (Week 1)
- [x] **tb-health-fix** – Fixed ThingsBoard health check: `/api/noauth/health` returns 404, switched to root URL. Needs HF Space redeploy
- [x] **hf-redeploy** – Redeployed HF Space via `huggingface_hub` API (git push blocked by binary file policy). Uploaded 8 files, space auto-rebuilt
- [x] **redis-verify** – Smoke test against live HF URL: 11/12 pass (control plan 404 expected β€” no cron run yet). Redis connected, IMS/Gemini configured
- [x] **weather-fix** – Fixed WeatherService crash: Redis deserialised DataFrame as dict/list β†’ `.empty` AttributeError. Added type guard + skip DataFrame Redis serialisation
- [x] **uptime-monitor** – UptimeRobot configured on `/api/health` (5-min interval). Added HEAD method support to health endpoint for monitoring compatibility
- [x] **monitoring-page** – Redesigned Alarms page β†’ Monitoring & Alarms: overall status banner, 3 data source cards (IMS/TB/Energy), backend services panel (Redis/TB/IMS/Gemini), data flow issues surface as system alarms. Sidebar renamed Alarms β†’ Monitoring
- [x] **code-review-fixes** – Fixed email alerter string formatting bug (CRITICAL), duplicate className in Home.tsx, DataFlowStatus null safety, energy price consistency (0.162 USD/kWh), table accessibility (scope attrs), negative age logging
- [x] **data-flow-monitor** – New `/api/health/data-sources` endpoint returning per-source green/yellow/red status (IMS weather, TB sensors, TB energy) with age tracking and configurable thresholds. `DataFlowMonitor` in `backend/services/data_flow_monitor.py`. Thresholds in `config/settings.py`: IMS yellow 60m/red 180m, TB yellow 15m/red 60m
- [x] **data-flow-alerts** – `EmailAlerter` in `backend/services/email_alerter.py`: SMTP email alerts when sources go red, per-source cooldown (60 min). Activated by `SMTP_HOST` + `ALERT_EMAIL_TO` env vars. Background loop in `main.py` checks every 5 min, pushes SSE `health` event
- [x] **data-flow-frontend** – `DataFlowStatus` component on Home page: colored dots (green/yellow/red) for each source, hover tooltips, auto-refresh via SSE. Hook: `useDataFlowStatus`, types: `DataSourceStatus` / `DataFlowStatusResponse`
- [x] **chatbot-range-validation** – `validate_numeric_ranges()` in guardrails.py: checks tool results against physical bounds (temp, GHI, PAR, VPD, CO2, power, etc.), flags sensor faults as caveats
- [x] **chatbot-cross-source** – `check_cross_source_consistency()` in guardrails.py: compares IMS vs TB temperature readings, warns when they diverge by >5Β°C
- [x] **chatbot-rag-scoring** – Improved `retrieve_relevant_rules()`: weighted scoring (exact match +2, partial word overlap +0.5), increased max_rules to 6
### 12.2: Short-term (Week 2–4)
- [x] **responsive-polish** – Sidebar hamburger already implemented (lg breakpoint). Fixed: Power chart responsive height (280/350/450px), Shading heatmap mobile min-width (400px), Home grid md breakpoint for map/camera/power row, camera/map responsive min-heights
- [x] **load-test** – `scripts/load_test.py` (Locust): 16 weighted endpoints, 10 users Γ— 1 min = 157 requests, p50=250ms, p95=1700ms, 0 errors. All endpoints healthy except `/api/health/data-sources` (not yet deployed at test time)
- [x] **circuit-breaker** – Added `CircuitBreaker` class (3 failures in 60s β†’ open for 5 min) to VineSensorService and EnergyService. Prevents retry storms when TB is down
- [x] **tracker-timeout** – Added exponential backoff retry (3 attempts: 1s, 2s) for TB shared attribute writes in TrackerDispatcher
### 12.2b: Code quality improvements (done 2026-03-29)
- [x] **code-review-critical** – Fixed: Redis health try-catch, chatbot init race condition (lock before null check), background task try-finally, daily planner IST timezone, JWT login parse safety, error boundary on Outlet
- [x] **error-standardization** – Extracted `check_service_error()` to `backend/api/utils.py`, used by energy + sensors routes. Fixed energy route ignoring errors when `daily_kwh` present
- [x] **control-auth** – Added `optional_auth` to all `/control/*` endpoints (status, plan, budget)
- [x] **farquhar-sigmoid** – Replaced hard 30Β°C RuBPβ†’Rubisco cutoff with smooth sigmoid (28–32Β°C). New "Transition" state. Prevents flip-flopping near threshold
- [x] **divergence-fix** – Fixed inverted logic in `control_loop.py:704` divergence check
- [x] **frontend-extract** – Extracted `StatCard` to `/components/StatCard.tsx`, date formatters to `/lib/dateUtils.ts`, energy price to `/config/constants.ts`. Home.tsx reduced by ~50 lines
- [x] **strict-types** – Added TypeScript interfaces: `ChillUnitResponse`, `SoilMoistureRow`, `RainRow`, `TrackerInfo`, `ControlTrackers`, `ControlBudget`. Replaced inline `any` types
- [x] **chart-memoize** – `useMemo` on tempData chart transformation in Home.tsx
### 12.2c: Architecture refactors (done 2026-03-29)
- [x] **vectorize-farquhar** – `compute_all()` rewritten with vectorized pandas/numpy (no iterrows). 10K rows: 7.5ms (was ~750ms). Numerically identical output (max diff=0)
- [x] **control-loop-di** – `ControlLoop.__init__()` accepts all 8 dependencies as keyword args. Pass None (default) for lazy auto-create; pass mocks for testing
- [x] **gate-pipeline** – `InterventionGate.evaluate()` split into 5 methods (`_check_meaningful_sun`, `_check_heat_stress`, `_check_water_stress`, `_check_radiation_load`, `_check_biology`) composed as pipeline. First rejection short-circuits
- [x] **budget-audit** – `src/budget_audit.py`: `BudgetAuditLog` appends one parquet row per control tick. `daily_summary(date)` and `weekly_report()` for compliance verification. Wired into `control_loop.py` tick step 12. CLI: `python -m src.budget_audit --report`
- [x] **chatbot-hebrew** – Added 18 Hebrew keywords to query classifier. System prompt: language matching, phenology-first, conciseness. IMS stale β†’ auto-supplement with TB sensors. Confidence rule override for dormancy/blocks
- [x] **refactor-tests** – 23 new tests in `tests/test_refactors.py`: vectorized vs scalar (4), sigmoid transition (2), gate pipeline (6), ControlLoop DI (3), budget audit (3), Hebrew classification (3), confidence override (2)
### 12.2e: Data + energy-model refresh β€” done 2026-05-18
- [x] **ims-refresh** – Pulled IMS station 43 from 2025-12-01 β†’ 2026-05-10 (10,748 rows, 15-min). API has ~8-day lag; data ends 2026-05-10.
- [x] **scripts-refresh-energy-data** – New `scripts/refresh_energy_data.py`: joins TB Plant `production` (1h SUM agg) with IMS hourly weather, reindexes to contiguous hours (nighttime = 0 kWh), adds time + solar-geometry features, writes `Data/energy_weather_merged.csv` (3,477 rows Γ— 18 cols).
- [x] **scripts-train-energy** – New `scripts/train_energy_predictor.py`: temporal hold-out (last 14 days), sklearn `GradientBoostingRegressor` + `LinearRegression` fallback (xgboost requires libomp on macOS; not installed). Bundle slot named `xgb_model` for backward compat with `EnergyPredictor.__init__`.
- [x] **energy-backtest-fix** – `EnergyPredictor.backtest()` now reads weather from `IMSClient` instead of the dead Air1 device.
- [x] **energy-model-validation** – 1,343 train rows / 337 hold-out rows. Hourly MAE = 4.5 kWh. **Daily mean signed error = +50%** β€” model systematically over-predicts in late April/May. Cause: model has no `tracker_angle` feature, so it can't see the agrivoltaic interventions that actively detune trackers as growing season starts. See 12.5 below for the planned fix.
- [x] **energy-tracker-feature-attempted** – Wired `tracker_angle_mean` + `tracker_angle_std` + `abs_tracker_angle` from TB into the refresh pipeline (`scripts/refresh_energy_data.py:fetch_tb_tracker_angles`). Two variants tried: (a) with imputation of pre-Feb history via hour-of-day medians β†’ bias worsened to **+57%**; (b) without imputation + dropped `sin_elevation` β†’ bias worsened to **+75%**. **Root cause is distribution shift, not features**: training tracker_angle avg = -15Β° (mild), hold-out avg = -51Β° (aggressive interventions). The model can't extrapolate operational regimes it didn't see in training. Reverted features to the original best config. Tracker telemetry is still collected into the CSV (columns `tracker_angle_mean`/`_std`) β€” ready to be consumed once 2026 growing-season data accumulates. See 12.6 / `bigml-tracker-feature` for the proper fix path.
### 12.2d: 2026 sensor migration β€” done 2026-05-18
- [x] **tb-2026-registry** – Clean-replaced `DEVICE_REGISTRY` in `src/data/thingsboard_client.py` with the 2026 fleet (35 entries: 19 Crop_2Soil + 12 Thermocouples + 4 Trackers). Legacy `Air1-4`, `Crop1-7`, `Soil*`, `Irrigation1`, `Thermocouples1-2` removed. Added `DeviceInfo.position` field (north/south/center/etc.). Convenience subsets `TREATMENT_DEVICES`/`REFERENCE_DEVICES`/`CROP_2SOIL_DEVICES`/`THERMOCOUPLE_DEVICES` derived at import. Treatment = rows 501/502/503/504/509; Reference = row 202.
- [x] **vinesnapshot-2026** – Rewrote `VineSnapshot` with 61 fields. Removed PAR/DLI/VPD/CO2/Irrigation (no 2026 sensor); added NDVI/PRI/PSRI/SIPI/GCI/LCI/DUVI ratios, dual soil profile (shallow + deep moisture/temp/pore-water EC), `panel_temp_active_count`. Legacy field names kept as None-valued compat shims so downstream consumers don't crash. Per-position keys use `{row}-{cardinal}` form (e.g. `"502-south-east"`).
- [x] **vinesnapshot-aggregation** – `get_vine_snapshot()` no longer hard-codes device names; iterates `DEVICE_REGISTRY` filtered by `area`. Full fleet fetched in 1.1 s via `ThreadPoolExecutor`.
- [x] **bounds-fixes** – Added physical bounds for PSRI/SIPI/GCI/LCI/DUVI/leaf-wetness in `_BOUNDS`. **Critical fix**: GCI was being clipped to `[-1, 1]` (real range 0..30), causing all daytime values to be discarded. Split `treatment_soil_pore_water_ec` into shallow + `_deep`. De-duped the `treatment_pri` computation.
### 12.3: Medium-term (Month 1–2)
- [ ] **chatbot-feedback** – Review first 20 thumbs-down/flag submissions, identify top 3 guardrail failures, tune rules
- [ ] **model-versioning** – Create `model_registry/` to track Chronos/ML/Farquhar parameter versions, enable A/B testing
- [ ] **phenology-validation** – Cross-reference GDD-based stage estimates vs camera observations, refine thresholds for Sde Boker
### 12.5: 2026 sensor follow-ups (queued after the foundational registry rewrite)
Bugs and design debt surfaced during the May 2026 sensor migration. Foundational
work landed in 12.2d above; these are the deferred items.
**Remaining bugs**
- [ ] **panel-tc-semantics** – `panel_temp_active_count` is per-device but the panel-temp avg is per-channel β€” inconsistent. Pick one. (`src/data/thingsboard_client.py:get_vine_snapshot`)
- [x] **tab-system-status-2026** – `ui/tab_system_status.py` `_render_fruiting_zone` rewritten to use NDVI / PRI / leaf-air Ξ”T / PSRI; risk assessment now triggers on Ξ”T > 3Β°C and PSRI > 0.2 in addition to canopy temp. Done 2026-05-18.
- [ ] **frontend-types-2026** – `frontend/src/types/api.ts` and the React hooks (`useSensors`, etc.) still declare `treatment_par_umol: number | null` and similar. After the migration these are always null β€” type narrows to `null`. Audit `frontend/src/lib/` + `frontend/src/hooks/` and remove or mark deprecated.
**Architectural concerns (numbered to match the post-migration code review)**
- [ ] **2026-A-auto-sync-registry** – Hardcoded `DEVICE_REGISTRY` is brittle: each TB change requires a code edit + redeploy. Build `scripts/sync_devices.py` that reads the `Crop 2Soil 2026` + `Thermocouples 2026` entity groups, writes `Data/devices.json`, and have `thingsboard_client.py` prefer that file with the hardcoded dict as offline fallback. Position decoding belongs in this sync script (regex over the Hebrew TB labels: `Χ¦Χ€Χ•ΧŸ`β†’north, `דרום`β†’south, `ΧžΧ–Χ¨Χ—`β†’east, `ΧžΧ’Χ¨Χ‘`β†’west, `ΧžΧ¨Χ›Χ–`β†’center).
- [ ] **2026-B-vinesnapshot-nest** – `VineSnapshot` is 61 flat fields and growing. Nest as `snapshot.treatment.{air,canopy,soil_shallow,soil_deep}` and `snapshot.panel`, expose flat `to_dict()` only at the JSON boundary. Defer until you're already breaking the API for another reason.
- [ ] **2026-C-schema-version** – Compat shims (`treatment_par_umol=None` etc.) silently say "sensor down" when the truth is "this metric no longer exists." Add `schema_version: "2026.1"` to the snapshot and let consumers branch / error explicitly; remove the shims one release later.
- [ ] **2026-D-split-fetch-aggregate** – `get_vine_snapshot()` mixes ~80 lines of I/O with ~70 lines of transformation. Split into `_fetch_all_latest(plan) β†’ raw_dict` + `_aggregate_snapshot(raw, now) β†’ VineSnapshot`. Then unit-test the aggregate step with fixture dicts (currently no test covers it).
- [ ] **2026-E-position-source-of-truth** – Position labels were decoded by hand from Hebrew TB labels β€” not reproducible. After 2026-A lands, decode at sync time. Until then, store the original TB label as `DeviceInfo.tb_label_hebrew` so the human-readable source survives.
- [ ] **2026-F-per-device-freshness** – `DataFlowMonitor` checks aggregate "newest TB timestamp" β€” one fast-reporting device makes the fleet look healthy even if 11 thermocouples are silent. Move to per-device staleness with grace per device-type (Crop_2Soil ~15min, Thermocouples up to ~60min due to deep-sleep cycles).
- [x] **2026-G-layout-regen** – `scripts/import_layout.py` updated to handle 2026 rows (202 reference; 501/502/503/504/509 treatment) and the `DeviceInfo.position` field via `_POSITION_OFFSETS`. `Data/layout.json` regenerated: 6 rows, 35 devices, each with `position_m: [x, y, z]`. Done 2026-05-18.
- [ ] **2026-H-position-taxonomy** – Position strings (`"south-east"`, `"center-east"`, …) are free-form. Convert to a `CardinalPosition` enum in `src/data/data_schema.py` (or document the canonical form in CLAUDE.md) so future labels match.
- [ ] **2026-I-aggregation-tests** – No unit test covers the new aggregation logic. Add at least 3 fixture-based tests in `tests/test_thingsboard_client.py`: full fleet, 1 device per area, all-empty. Depends on 2026-D.
**Follow-on integration work**
- [ ] **2026-J-ims-ambient-bridge** – Wire IMS Sde Boker station 43 β†’ `VineSnapshot.ambient_*` fields so they stop returning None. Currently `Alarms.tsx` and `Home.tsx` fall through to `treatment_air_temp_c`, which is misleading (it's not ambient).
- [ ] **2026-K-chatbot-spectral-tools** – Add chatbot tools `get_vegetation_indices()` and `get_canopy_health()` exposing the new 2026-only data (NDVI/PRI/PSRI/SIPI per row + position).
- [ ] **2026-L-spectrometer-page** – New frontend page (or extend `Agro.tsx`): vegetation-index time series, NDVI/SIPI heatmap across the 17 active Crop_2Soil devices, panel surface-temp 4Γ—12 grid from the thermocouples.
### 12.6: "Big" β€” Stage 2 photosynthesis ML retrain on 2026 schema
Plan, not yet executed. The Stage 2 ML predictor (`src/forecasting/predictor.py`)
forecasts A_n (photosynthesis rate) from weather features. Currently trained on
`Data/Seymour/sensors_wide.csv` (frozen at 2026-02-01) β€” features include PAR,
airTemperature, CO2, VPD, leafTemperature from the legacy Air1/Crop fleet.
The 2026 sensor refresh deprecated all of those sensors. The new fleet emits
NDVI/PRI/PSRI/spectrometer bands + dual-soil profile + IRT ambient β€” a
fundamentally different feature space. This is a redesign, not a refresh.
**Phase 1 β€” Data collection pipeline (2-3 days work)**
- [x] **bigml-collection** – `scripts/collect_2026_training_data.py` written
and smoke-tested 2026-05-18. Pulls 15-min telemetry from the 10 treatment +
7 reference Crop_2Soil devices via TB, averages per area per timestep,
inner-joins with IMS 15-min weather, writes to
`Data/2026/sensor_history.parquet`. Append-mode dedupes on
(timestamp, area). May 1–10 sample: **1,896 rows** (948 per area).
- [x] **bigml-stage1-2026** – Farquhar inputs derived from the 2026 fleet:
PAR β‰ˆ 2.0 Γ— GHI (Akitsu et al. 2017, Β±10 %), VPD via Tetens from IMS
RH + Tair, CO2 = 420 ppm assumed, CWSI = (Tleaf βˆ’ Tair)/15 clipped to
[0, 1]. `compute_farquhar_a()` runs `FarquharModel.calc_photosynthesis()`
row-by-row; result column `a_farquhar_umol`. Validation: daytime mean
13.1 Β΅mol COβ‚‚/mΒ²/s, peak 19.6 Β΅mol β€” squarely in Greer & Weedon's
empirical Semillon range for the leaf-temp window seen.
- [x] **bigml-collection-shading-correction** – `apply_shading_correction()`
added to `scripts/collect_2026_training_data.py`. For each treatment-area
timestep: pulls `tracker_angle_mean` (or astronomical fallback when telemetry
missing), runs `ShadowModel.project_shadow()` + `compute_par_distribution()`,
takes the fruiting-zone mean PAR, and multiplies the IMS-derived
`par_umol_derived` by that factor. Reference rows untouched. May 1–10 sample:
treatment PAR factor distribution is 0.10–1.0 (median 0.82, mean 0.73) β€”
panels meaningfully reduce fruiting-zone PAR. Farquhar A label is recomputed
after the correction. Done 2026-05-18.
- [ ] **bigml-baseline** – Until enough 2026 data accumulates (β‰₯3 months
growing-season hours), run Farquhar-only as the photosynthesis estimate
(skip Stage 2 ML for 2026). The chatbot's photosynthesis tool already
supports this via `PhotosynthesisService.compute_fvcb()`.
- [ ] **bigml-collection-cron** – Schedule
`scripts.collect_2026_training_data` to run weekly via GitHub Actions
(similar pattern to `control-tick.yml` / `daily-planner.yml`). Window =
last 8 days (overlaps to be safe against IMS's ~8-day publication lag).
**Phase 2 β€” Feature engineering (1-2 days)**
- [ ] **bigml-features** – Define the 2026 ML feature set. Candidates:
IMS GHI/Tair/RH/wind (forecastable), Crop_2Soil NDVI/PRI/PSRI/SIPI (now),
soilMoisture (shallow + deep), leafTemperature, tracker angle (current),
hour, doy_sin/cos. Decide whether to keep the per-device dimension (input
= 17 device readings per timestep) or aggregate to area-level (~5 features).
- [ ] **bigml-spectral-pca** – With 17 spectrometer bands per device Γ— 17
devices = 289 columns, dimensionality reduction is needed. Run PCA on the
spectral subset and keep the top 5-8 components.
**Phase 3 β€” Training + validation (1 day)**
- [ ] **bigml-train** – Train RF / GBR / Linear regressors (analogous to
Stage 2 v1). Use temporal hold-out from the latest 14 days of the
collected 2026 data. Compare MAE vs Farquhar-only baseline.
- [ ] **bigml-tracker-feature** – Add `tracker_angle` and
`intervention_active` as features to the **energy predictor too** (fixes
the +50% over-prediction bias seen in 12.2e). Requires pulling tracker
telemetry alongside production into the energy refresh script.
- [ ] **bigml-deploy** – Wire the trained model into
`PhotosynthesisService.predict_ml()`, replacing the current
Stage 2 v1 model. Update model_versioning in `model_registry/`
(Phase 12.3 β€” `model-versioning`).
**Dependencies / open questions**
- Need β‰₯3 months of 2026 growing-season data before Phase 3 is statistically
meaningful. With data collection starting May 2026, retraining is realistic
for **August–September 2026**.
- CO2 atmospheric assumption (420 ppm) is conservative but the lack of a
real sensor is a real degradation vs the legacy fleet. Consider sourcing
hourly CO2 from a nearby Mauna Loa proxy if precision matters.
- Tracker-angle feature needs ThingsBoard telemetry alignment β€” trackers
emit `angle` as shared-attribute, not telemetry by default. Verify the
control loop is writing telemetry to TB.
### 12.7: Frontend redesign β€” Claude Design handoff (done 2026-05-18)
- [x] **redesign-foundation** β€” Replaced `:root` tokens with Negev sand + vine green + sun gold; loaded Source Serif 4 + IBM Plex Sans + IBM Plex Mono; added brand stripe motif + masked-PNG wordmark; re-targeted shadcn aliases. `tailwind.config.ts` extended with serif/mono families and brand color tokens.
- [x] **redesign-layout** β€” Rewrote `AppSidebar` to a 240 px sand sidebar with 4-section nav (Vineyard / Energy / Operations / Knowledge), masked logo, brand stripes, vine left-bar on active. New `Topbar.tsx` (breadcrumb Β· ⌘K search Β· refresh Β· bell Β· avatar). New `PageHeader.tsx` (eyebrow + serif H1 with optional italic emphasis + deck + 3-row meta panel). `AppLayout` adds Topbar + footer hairline strip.
- [x] **redesign-shared-components** β€” New `Sparkline`, `CardHead`, `DataFlowRail`, `TrackerArc`, `PlanTimeline` (96-cell ribbon), `StressHeatmap`. `StatCard` rewritten with eyebrow + delta + serif tabular number + caption + Sparkline. New chart wrappers `charts/SwAreaChart.tsx` and `charts/SwLineChart.tsx` with shared SolarWine defaults (hairline grid, mono ticks, brand palette, dashed reference lines).
- [x] **redesign-home** β€” Editorial header, data-flow rail, 4-stat row, 96-slot plan ribbon + power chart, tracker fleet with arc dials + status pills, FvCB-vs-ML photosynthesis card, stress heatmap, **dark Advisor card** with sun-gold "Apply at next tick" CTA, 7-day climate w/ 32Β°C dashed line, chill-accumulation bars.
- [x] **redesign-pages-all** β€” All 10 remaining pages rewritten:
- **Agronomy** β€” 4-stat row + A1–A4 block table w/ vigor bars + soil profile + agronomy log + water budget
- **Photosynthesis** β€” model-comparison big numbers + Chronos forecast + sensor explorer
- **Shading** — 64 px serif offset slider w/ vine→sun gradient, 3-zone canopy diagram, stress×offset map, 8-rule policy grid
- **Power** β€” 4-cell KPI strip + today-vs-yesterday-vs-astro line, inverter cards, weekly bars, revenue ledger
- **Trackers** β€” fleet table w/ arc dials + status pills + 24h sparklines, dispatch log, reliability fields
- **Control loop** β€” 5-stage pipeline with active highlight, L0..L4 priority stack, hierarchical energy budget, plan ribbon
- **Advisor** β€” chat surface w/ confidence pills + citations + feedback, dark day-ahead brief, active recommendations, quick-asks
- **Monitoring** (was Alarms) β€” alert stream w/ severity pills + ack, 6-source health column
- **Documentation** β€” numbered serif accordion (10 biology rules), API ref table (21 endpoints), 12-term glossary
- **Research** (new) β€” vineyard-photo hero band, 3 paper cards, datasets table, 7-person crew strip. `/research` route added.
- [x] **redesign-build** β€” TypeScript clean, vite build 2.5 s, 240 KB gzipped JS. Dev server at http://localhost:8080/.
**TODO markers inline (would benefit from new backend endpoints)**
- [ ] **redesign-stress-grid** β€” `/api/photosynthesis/stress-grid?date=...` for live 4Γ—24 CWSI grid (used by Home + Shading; currently synthetic).
- [ ] **redesign-inverters** β€” `/api/energy/inverters` for per-inverter live state on Power.
- [ ] **redesign-dispatch-stats** β€” `/api/control/dispatches` summary for p95 ack latency on Trackers.
- [ ] **redesign-events-dispatch** β€” surface dispatch + alert events through `/api/events/*` so the Trackers log + Monitoring stream are live, not seeded.
- [ ] **redesign-frontend-types-purge** β€” `frontend/src/types/api.ts` and hooks still declare `treatment_par_umol: number | null` etc. After the 2026 fleet migration these are always null; trim or mark deprecated.
### 12.8: Streamlit staleness audit (done 2026-05-18)
- [x] **streamlit-control-replay-freshness** β€” `ui/tab_system_status.py:_render_control_replay`: empty-state message now uses today's date; loaded log shows a freshness warning when older than 30 days; caption surfaces the simulated date range (sim_2026-05-01 β†’ 2026-05-10).
- [x] **scripts-sim-defaults** β€” `scripts/run_control_simulation.py` defaults changed from `2025-07-01 / 2025-07-07` to `today βˆ’ 7 d / today` so future runs are fresh.
- [x] **fresh-sim-log** β€” Generated `Data/simulation_logs/sim_2026-05-01_2026-05-10.json` (960 ticks). Required installing `astral` into the venv (was missing; fixed 960/960 errors on the first attempt).
- [x] **streamlit-shading-caption** β€” Added "historical analysis Β· `sensors_wide.csv` frozen at Feb 2026" caption to the Shading tab so the date picker isn't read as live.
- [x] **camera-caption** β€” Camera image caption now says "if the view shows only sky, the on-site camera needs realignment". Physical site issue; can't be fixed from code.
### 12.4: Long-term (3+ months)
- [ ] **multi-vineyard** – Refactor `config/settings.py` to multi-tenant pattern (vineyard ID β†’ settings), add Postgres (Neon free tier)
- [ ] **licor-calibration** – Ingest first real LiCor measurements, retrain Farquhar Vcmax/Jmax via `src/calibration_pipeline.py`
- [ ] **chronos-vs-ml** – 3-month live comparison of Chronos-2 vs ML ensemble, promote winner to primary predictor
---
## Session notes
### 2026-02-27 β€” Phase 8B: Gemini LLM Data Engineering
- Created `src/llm_data_engineer.py` with `LLMDataEngineer` class.
- `analyze_anomalies()`: constructs a structured prompt from per-column descriptive stats + site-specific domain context (physical ranges, Negev notes) β†’ Gemini returns a JSON with `lower_bound`, `upper_bound`, `zscore_threshold`, `iqr_multiplier`, `rationale` per column; falls back to physics-based bounds when API unavailable.
- `apply_cleaning()`: applies union of bound violations + Z-score + IQR anomaly flags; three strategies: `clip` (default), `nan`, `drop`. Detected 14 PAR anomalies (0.05%), 9 VPD anomalies (0.03%), 168 COβ‚‚ anomalies (0.58%) on `sensors_wide_sample.csv`.
- `engineer_features()`: adds `hour_sin`, `hour_cos`, `doy_sin`, `doy_cos` (cyclical encodings); `stress_risk_score = vpd_weight Γ— norm(VPD) + cwsi_weight Γ— norm(CWSI)` in [0, 1]; weights sourced from Gemini (default: 0.6/0.4, falls back gracefully without CWSI).
- `run_pipeline()`: single-call orchestration of anomaly detection β†’ cleaning β†’ feature engineering.
- Added `google-genai>=1.0` to `requirements.txt`.
### 2026-02-27 β€” Dashboard & pvlib
- **Data Explorer:** Intro text now lists all three data sources (Vineyard sensors, Weather station data, **AI Data Engineering**). Third option runs full LLM pipeline in-tab (thresholds, cleaning summary, before/after PAR/VPD, feature spec, stress profile); no cache loader, all precomputed on selection.
- **Presentation tab:** β€œWhat’s Next” completed list includes **Gemini LLM data engineering** with pointer to Data Explorer β†’ AI Data Engineering. β€œWhat each tab shows” updated so Data Explorer bullet mentions AI Data Engineering. Key takeaways add Gemini data engineering (anomaly cleaning + stress/time features).
- **pvlib:** `src/solar_geometry.py` β€” `compute_tracker_tilt()` now calls `pvlib.tracking.singleaxis()` with first two args positional (zenith, azimuth) so it works with both pvlib &lt; 0.13.1 (`apparent_azimuth`) and β‰₯ 0.13.1 (`solar_azimuth`). Fixes β€œShadow model demo unavailable: singleaxis() got an unexpected keyword argument 'solar_azimuth'”.
### 2026-03-05 β€” ThingsBoard live telemetry integration (Step 1)
- Rewrote `src/thingsboard_client.py`: full device registry (22 devices, UUIDs from devices.csv), area classification (treatment rows 501–502 / reference rows 503–504 / ambient Air1), parallel fetch with `ThreadPoolExecutor`, JWT auth with token refresh.
- Added `VineSnapshot` dataclass: aggregated treatment vs reference readings β€” Air, Crop (per panel position: west-bottom/east-upper/east-bottom/west-upper), Soil, Irrigation, Thermocouples. Includes `to_advisor_text()` and `to_dict()`.
- Added `_bounded_avg()` with `_BOUNDS` dict: physical plausibility filter rejects sensor faults (e.g. Soil5 thermocouple reading 653Β°C). Soil, Air, PAR, NDVI, VPD, COβ‚‚ all filtered by domain bounds.
- Wired into `src/vineyard_chatbot.py`: new `_tool_get_vine_state()` tool + `get_vine_state` in system prompt + dispatch router; `_tool_day_ahead_advisory()` now passes `VineSnapshot` to advisor.
- Wired into `src/day_ahead_advisor.py`: optional `vine_snapshot` param in `advise()`; `to_advisor_text()` inserted before weather forecast in Gemini prompt.
- TB credentials (`TB_USERNAME`, `TB_PASSWORD`) added to `.env`; config also accepts `THINGSBOARD_USERNAME`/`THINGSBOARD_PASSWORD`.
- Tested live: all 22 devices fetched in 1.1 s, PAR shading ratio 0.33 (67% reduction), soil temp 15.5Β°C (outlier-filtered).
- Synced Phase 9 checklist: all chatbot tasks marked done (implemented in a prior session, todo file not updated).
### 2026-03-09 β€” Phase 10: Anti-Hallucination Guardrails
- Created `src/chatbot/guardrails.py` with 4 components: QueryClassifier (regex data/knowledge/greeting routing), ResponseValidator (deterministic rule checks with block/warn severity), estimate_confidence (data freshness β†’ confidence level), tag_tool_result (source metadata injection).
- Rewrote `src/chatbot/vineyard_chatbot.py`: integrated all guardrails into `chat()` flow β€” query classification β†’ forced tool re-prompting β†’ source-tagged tool results β†’ response validation β†’ confidence estimation. Extended `ChatResponse` with confidence/sources/caveats/rule_violations. Strengthened system prompt: "NEVER invent sensor readings", "MUST call a tool for data questions", "cite source and timestamp".
- Updated `ui/tab_advisor.py`: new `_render_grounding_metadata()` renders colored confidence badge, source citations, caveats, and rule violation warnings under each chat response. Metadata persisted in chat history.
- Added 23 new tests to `tests/test_advisor_chatbot.py`: 5 adversarial QA scenarios (shade-before-10 blocked, data-question forces tool call, stale-data warns, no-shade-without-reason warns, tool-failure returns insufficient_data) + unit tests for all guardrail components. Total: 42 tests, all passing.
- Updated `context/2_plan.md` with Phase 10 architecture, validation rules, confidence mapping, and remaining steps. Updated dependency order.
### 2026-02-17 β€” Code quality fixes
- Fixed `Preprocessor.merge_ims_with_labels` position-based fallback: now raises `ValueError` instead of silently misaligning rows when `timestamp_index_labels=False` and labels have no named index.
- Removed all ~50 deprecated `use_container_width=True` calls in `app.py` (Streamlit 1.54): `st.image` β†’ `width='stretch'`; `st.plotly_chart` / `st.dataframe` β†’ argument removed (default is already `'stretch'`).
- Fixed follow-up `SyntaxError`: regex removal of `use_container_width=True` in the middle of argument lists left double commas (`,,`); collapsed all `,,` β†’ `,`.
- Updated `context/CODE_REVIEW.md` sections 16–17 with applied fixes and remaining work list.
- Updated "What's next" sections in `app.py` (tab_photosynthesis section 6 and About tab) to reflect Chronos-2 and Gemini routing as completed.