# Project export: EnergyX

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: An AI copilot for US energy markets that explains price spikes in real-time using ISO data, multi-agent LLM analysis, and ML forecasting.
- Devpost: https://devpost.com/software/energyx
- GitHub: https://github.com/adi-kulkarni1/TreeHacks2026
- Video: https://www.youtube.com/embed/16ZVsocsNzo?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Claude Opus 4.6 (6 commits), AidanTiruvan (6 commits), Adi Kulkarni (5 commits), Noah Dee (3 commits), Cursor (2 commits)

## Devpost submission (written by the team)

### Inspiration

Energy markets are notoriously volatile, and a single price spike can cost utilities and traders millions. Yet understanding why a spike happened requires cross-referencing real-time prices, grid load, fuel mix, and weather data across multiple sources. We wanted to build an AI copilot that could do this analysis in seconds, using only free, public data. This democratizes energy market intelligence that typically costs $50K+/year from incumbent platforms. Identification of these spikes also provide opportunities for more sustainable energy consumption.

### What it does

EnergyX monitors ERCOT and CAISO power markets in real-time and answers three questions: what happened, why, and what's likely next. It features: Live Dashboard with WebSocket price feeds, load curves, and fuel mix charts AI Spike Explainer that uses a multi-agent LangGraph pipeline (Data, Analysis, Narrative, Verification) to produce evidence-backed explanations of price anomalies ML Forecasting with XGBoost models that predict next-interval prices and spike probabilities Geographic Grid Map showing live node prices and spike hotspots RAG-powered Search using ChromaDB and Elasticsearch to find historical patterns Automated Daily Briefs and configurable email alerts

### How we built it

Backend: FastAPI with async SQLite for low-latency data access. We built a data pipeline using the open-source gridstatus library to ingest ISO market data and NWS weather forecasts on a 15-minute schedule, with a 45-second WebSocket live feed. AI Layer: A LangGraph multi-agent pipeline where specialized agents collect data, run spike detection (z-score based), build context packs, generate narratives via GPT-5, and cross-verify results using a secondary Mistral 7B model on RunPod. Spike explanations are indexed into ChromaDB and Elasticsearch for RAG retrieval for future analysis. ML: XGBoost models trained on engineered features (price lags, rolling stats, hour/day cyclical encodings, momentum indicators) for price regression and spike classification with 90% confidence intervals. Frontend: React 19 + TypeScript with Tailwind CSS, Recharts for interactive visualizations, SSE streaming for real-time AI explanations, and a responsive sidebar layout with live connection indicators and toast alerts.

### Challenges we ran into

Market data reliability: ISO APIs are slow and inconsistent, so we implemented a DB-first, live-fallback strategy with stale-while-revalidate caching to handle outages gracefully. LLM verification: Single-model explanations sometimes gave us hallucinated drivers. We added a verification loop in LangGraph and cross-verification via a secondary model to catch errors. Timezone complexity: ERCOT runs on Central time, CAISO on Pacific, so the solar generation relevance depends on local time (6 AM–7 PM), requiring careful timezone-aware logic throughout. SSL issues with CAISO: macOS Python's SSL certificates don't include CAISO's CA, requiring a certifi-based patch.

### What we learned

We learned that the hardest part of AI applications isn't the model, it's actually building reliable data pipelines and context assembly. The quality of our spike explanations improved dramatically when we invested in better context packs (correlating prices with load ramps, fuel mix shifts, and weather data) rather than prompt engineering alone. We also gained deep appreciation for multi-agent architectures where verification agents catch hallucinations that single-pass generation misses.

### What's next

Expand ISO coverage — Add PJM, NYISO, SPP, and MISO to cover all major US power markets, giving nationwide visibility into price dynamics. Real-time trading signals — Evolve spike predictions from informational alerts into actionable buy/sell signals with backtested confidence scores for energy traders. Fine-tuned energy LLM — Train a domain-specific model on our growing RAG corpus of indexed spike explanations and market briefs to reduce reliance on standard LLM models and lower our latency. Renewable integration forecasting — Add solar irradiance and wind speed forecasting models to predict how renewable generation ramps will impact prices before they happen. Mobile app with push alerts — React Native companion app so grid operators and traders get spike alerts and AI briefs on the go. Utility partnerships — Integrate with SCADA/DERMS data from willing utility partners to unlock even deeper root-cause analysis that public data alone can't provide. Historical pattern matching — Use our Elasticsearch index of past spike events to surface "this spike looks like X from last summer" comparisons, giving analysts instant historical context.

## README (from the GitHub repository)

# EnergyX — Real-Time Energy Market Intelligence Copilot

EnergyX answers "what happened, why, and what's likely next" in US power markets using public ISO/RTO and weather data. It produces evidence-backed spike explanations, alerts, and daily briefs — without requiring utility/SCADA/DERMS integrations.

## Quick Start

### Backend (Python / FastAPI)

```bash
cd backend

# Create virtual environment
python -m venv .venv
source .venv/bin/activate   # On Windows: .venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Set up environment variables
cp .env.example .env
# Edit .env with your OpenAI API key (required) and other credentials

# Run the server
uvicorn app.main:app --reload --port 8000
```

API docs available at: http://localhost:8000/docs

### Frontend (React / TypeScript)

```bash
cd frontend

# Install dependencies
npm install

# Run dev server (proxies /api to backend on port 8000)
npm run dev
```

Open http://localhost:5173 in your browser.

## Architecture

```
backend/
├── app/
│   ├── main.py              # FastAPI entry point
│   ├── config.py             # Environment config
│   ├── database.py           # SQLAlchemy async setup
│   ├── models/               # DB models (prices, load, weather, alerts)
│   ├── ingestion/            # Data pipeline (gridstatus + NWS)
│   │   ├── iso_fetcher.py    # ERCOT + CAISO data via gridstatus
│   │   ├── weather.py        # NWS API client
│   │   └── scheduler.py      # APScheduler for periodic ingestion
│   ├── analytics/            # Deterministic metrics + spike detection
│   │   ├── metrics.py        # Volatility, ramps, forecast error
│   │   └── spike_detector.py # Threshold-based anomaly detection
│   ├── intelligence/         # AI-powered analysis
│   │   ├── context_builder.py # Assembles "context pack" for LLM
│   │   ├── explainer.py      # OpenAI narrative generation
│   │   ├── daily_brief.py    # Automated morning report
│   │   └── screener.py       # Node/zone ranking
│   ├── alerts/               # Alert engine (Slack + email)
│   └── api/                  # FastAPI route handlers

frontend/
├── src/
│   ├── api/client.ts         # Typed API client
│   ├── components/           # Reusable UI (charts, cards, panels)
│   └── pages/                # Dashboard, Spike Explainer, Brief, Screener, Alerts
```

## Key API Endpoints

| Endpoint | Method | Description |
|---|---|---|
| `/api/market/prices` | GET | Fetch LMP/SPP prices |
| `/api/market/load` | GET | Fetch system load data |
| `/api/market/fuel-mix` | GET | Fetch generation fuel mix |
| `/api/explain/spike` | POST | Generate spike explanation |
| `/api/briefs/today` | GET | Get today's daily brief |
| `/api/screener/top` | GET | Get ranked locations |
| `/api/alerts/` | CRUD | Manage alert configurations |

## Data Sources (All Free / Public)

- **gridstatus** (open-source) — Unified Python interface to ERCOT, CAISO
- **NWS API** — Weather forecasts and alerts (no key required)
- **OpenAI API** — Narrative generation (requires API key)

## Team Workflow

The codebase is divided into 3 independent work streams:

1. **Data Pipeline** (`ingestion/` + `analytics/`) — ISO data, weather, metrics, spike detection
2. **AI Layer** (`intelligence/` + `alerts/`) — Context builder, LLM explainer, briefs, alerts
3. **Frontend** (`frontend/`) — Dashboard, spike explainer UI, charts, alert config


## Detected evidence (automated analysis)

Indexed codebase: 85 recognized source files, 479 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- LangChain (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (94 of 94)

```
.gitattributes
.gitignore
backend/.env.example
backend/app/__init__.py
backend/app/agents/__init__.py
backend/app/agents/graph.py
backend/app/agents/nodes.py
backend/app/agents/prompts.py
backend/app/agents/state.py
backend/app/agents/tools.py
backend/app/alerts/__init__.py
backend/app/alerts/alert_engine.py
backend/app/analytics/__init__.py
backend/app/analytics/metrics.py
backend/app/analytics/spike_detector.py
backend/app/api/__init__.py
backend/app/api/alerts.py
backend/app/api/briefs.py
backend/app/api/chat.py
backend/app/api/explain.py
backend/app/api/forecast.py
backend/app/api/map.py
backend/app/api/market.py
backend/app/api/models.py
backend/app/api/screener.py
backend/app/api/search.py
backend/app/api/ws.py
backend/app/config.py
backend/app/database.py
backend/app/forecasting/__init__.py
backend/app/forecasting/features.py
backend/app/forecasting/predictor.py
backend/app/geo/__init__.py
backend/app/geo/node_coordinates.py
backend/app/ingestion/__init__.py
backend/app/ingestion/iso_fetcher.py
backend/app/ingestion/scheduler.py
backend/app/ingestion/weather.py
backend/app/intelligence/__init__.py
backend/app/intelligence/context_builder.py
backend/app/intelligence/daily_brief.py
backend/app/intelligence/explainer.py
backend/app/intelligence/screener.py
backend/app/llm/__init__.py
backend/app/llm/router.py
backend/app/main.py
backend/app/models/__init__.py
backend/app/models/market_data.py
backend/app/rag/__init__.py
backend/app/rag/chroma_store.py
backend/app/rag/elasticsearch_store.py
backend/app/realtime/__init__.py
backend/app/realtime/live_feed.py
backend/app/realtime/ws_manager.py
backend/mcp_server.py
backend/requirements.txt
backend/test_brief.py
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/api/client.ts
frontend/src/App.tsx
frontend/src/components/AlertBanner.tsx
frontend/src/components/ChartErrorBoundary.tsx
frontend/src/components/ChatPanel.tsx
frontend/src/components/DateTimePicker.tsx
frontend/src/components/DriverCard.tsx
frontend/src/components/EvidencePanel.tsx
frontend/src/components/FuelMixChart.tsx
frontend/src/components/Layout.tsx
frontend/src/components/LiveIndicator.tsx
frontend/src/components/LoadChart.tsx
frontend/src/components/PriceChart.tsx
frontend/src/components/SpikeToast.tsx
frontend/src/contexts/WebSocketContext.tsx
frontend/src/index.css
frontend/src/main.tsx
frontend/src/pages/Alerts.tsx
frontend/src/pages/DailyBrief.tsx
frontend/src/pages/Dashboard.tsx
frontend/src/pages/Forecast.tsx
frontend/src/pages/MapPage.tsx
frontend/src/pages/Screener.tsx
frontend/src/pages/Search.tsx
frontend/src/pages/SpikeExplainer.tsx
frontend/src/utils/timezone.ts
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
PLAN.md
README.md
```

### Dependencies

- backend/requirements.txt: aiosqlite@>=0.20.0, apscheduler@>=3.10.0, certifi@>=2024.0, chromadb@>=0.5.0, elasticsearch[async]@>=8.0.0, fastapi@>=0.115.0, gridstatus@>=0.28.0, httpx@>=0.27.0, joblib@>=1.3.0, langchain-core@>=0.3.0, langchain-openai@>=0.2.0, langgraph@>=0.2.0, mcp@>=1.0.0, numpy@>=1.26.0, openai@>=1.50.0, pandas@>=2.2.0, pydantic-settings@>=2.6.0, python-dotenv@>=1.0.0, pytz@>=2024.1, scikit-learn@>=1.4.0, sqlalchemy[asyncio]@>=2.0.0, uvicorn[standard]@>=0.32.0, websockets@>=12.0, xgboost@>=2.0.0
- frontend/package.json: @eslint/js@^9.39.1, @tailwindcss/vite@^4.1.18, @types/node@^24.10.1, @types/react@^19.2.7, @types/react-dom@^19.2.3, @vitejs/plugin-react@^5.1.1, axios@^1.13.5, eslint@^9.39.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.24, globals@^16.5.0, lucide-react@^0.564.0, react@^19.2.0, react-dom@^19.2.0, react-router-dom@^7.13.0, recharts@^3.7.0, tailwindcss@^4.1.18, typescript@~5.9.3, typescript-eslint@^8.48.0, vite@^7.3.1

### Recent commits (newest first)

- updates
- Wire Elasticsearch RAG into chat agents, daily briefs, and startup seeding
- Merge main into MCP: remove PJM, add SendGrid config, resolve conflicts
- Merge main into MCP: teammate's chart UX, spike explainer, and timezone improvements
- Merge MCP branch into main: dual-model LLM, Elasticsearch RAG, error boundaries
- Merge branch 'main' of https://github.com/adi-kulkarni1/TreeHacks2026
- Fix spike explainer: timezone handling, driver detection, and LLM output quality
- Fix 7 backend bugs, add frontend resilience (retry, cache, error boundaries)
- Fix OpenAI model test temperature and timezone-aware datetime comparison
- Dual-model LLM (OpenAI + RunPod), Elasticsearch RAG, merge teammates' CAISO + daily digest
- daily digest feature
- Merge branch 'main' of https://github.com/adi-kulkarni1/TreeHacks2026
- caiso fuel mix
- load and fuel type
- explain spike on frontend (opt in to multi agent)
- Improve chart UX: readable axis labels, yellow selection overlay, loading status
- frontend lints [no major change]
- spike explainer 🤠
- Merge branch 'main' of https://github.com/adi-kulkarni1/TreeHacks2026
- backend data update and frontend fix

## Key source files (fetched from GitHub, selected and truncated for size)

### PLAN.md

```markdown
# Energy Real-Time Energy Market Intelligence Copilot

## Master Plan Document

---

## 1. Executive Summary

**One-sentence product:** EnergyX is a lightweight SaaS that answers "what happened, why, and what's likely next" in US power markets using public ISO/RTO and weather data, producing evidence-backed spike explanations, alerts, and daily briefs — without requiring utility/SCADA/DERMS integrations.

**One-sentence positioning:** "The first analyst copilot for energy markets that runs entirely on public data, explains every answer with citations, and deploys in minutes instead of months."

**What we sell:** Analyst productivity and decision support — not trading alpha, not a platform, not "AI."

**What we replace:** The 30-60 minute manual process of stitching ISO data, weather, outage notes, and writing up "why did prices move?" every single day.

---

## 2. The Opportunity

### Where This Idea Came From

Analysis of OATI's product suite (DERMS, Microgrids, Transmission, Energy Markets, Smart Meters, AI Genie) revealed that the energy/grid software space is large, compliance-heavy, and integration-dependent. OATI is strong where software touches real-time operations and regulated market plumbing.

**The problem with building on top of OATI or similar vendors:**
- Their systems require deep integrations (SCADA, OMS, DERMS)
- Access to these systems isn't free or publicly available
- You can't demo a working product without being inside a utility's infrastructure

**The insight:** There is a category of valuable intelligence work that runs entirely on *public* data — data that ISOs are required to publish. The hard part isn't accessing the data; it's stitching it together, explaining what it means, and doing it fast enough to be useful.

### The Market Gap

Most existing energy AI products fall into one of these buckets:

| Category | Examples | Why There's Still a Gap |
|---|---|---|
| Enterprise AI on proprietary data | Enverus AI, OATI Genie | Requires expensive datasets or deep operational integrations |
| ML forecasting platforms | PCI Forecaster, PLEXOS Pulse | Focused on prediction, not explanation; complex to deploy |
| Market data terminals | Yes Energy PowerSignals | Dashboards and data delivery, not "explain why" copilots |
| Internal-only tools | Utility in-house analytics | Not available as SaaS; not productized |

**What's missing:** A product that is (a) public-data-first, (b) explains with evidence, (c) fast to onboard, and (d) priced for smaller teams who don't want a six-figure enterprise platform.

---

## 3. Our Edge

### Edge A — Public-Data-First Onboarding (No Enterprise Integrations)

We go live using endpoints that are already published:
- **PJM API Portal** — free API key, Data Miner 2 APIs for prices/load/constraints
- **ERCOT** — publishes DAM/RTM prices, load forecast vs actual, SPPs publicly
- **CAISO OASIS** — automated data downloads via documented URL/API requests
- **NWS API** — free weather forecast and aler
[truncated — 19512 more characters]
```

### backend/requirements.txt

```
# EnergyX Backend Dependencies
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
pydantic-settings>=2.6.0
websockets>=12.0

# Database
sqlalchemy[asyncio]>=2.0.0
aiosqlite>=0.20.0

# Data pipeline
gridstatus>=0.28.0
pandas>=2.2.0
numpy>=1.26.0

# SSL certificates (needed for CAISO on macOS)
certifi>=2024.0

# HTTP client (for NWS API, Slack webhooks)
httpx>=0.27.0

# Scheduler
apscheduler>=3.10.0

# AI / LLM
openai>=1.50.0

# Multi-Agent Architecture (LangGraph)
langgraph>=0.2.0
langchain-openai>=0.2.0
langchain-core>=0.3.0

# RAG — Vector Store
chromadb>=0.5.0
elasticsearch[async]>=8.0.0

# ML Forecasting
xgboost>=2.0.0
scikit-learn>=1.4.0
joblib>=1.3.0

# Timezone support (required by gridstatus)
pytz>=2024.1

# MCP Server
mcp>=1.0.0

# Utilities
python-dotenv>=1.0.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.13.5",
    "lucide-react": "^0.564.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-router-dom": "^7.13.0",
    "recharts": "^3.7.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@tailwindcss/vite": "^4.1.18",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.7",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "tailwindcss": "^4.1.18",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.48.0",
    "vite": "^7.3.1"
  }
}

```

### frontend/src/main.tsx

```typescript
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';

createRoot(document.getElementById('root')!).render(<App />);

```

### frontend/src/App.tsx

```typescript
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { WebSocketProvider } from './contexts/WebSocketContext';
import Layout from './components/Layout';
import Dashboard from './pages/Dashboard';
import SpikeExplainer from './pages/SpikeExplainer';
import DailyBrief from './pages/DailyBrief';
import Screener from './pages/Screener';
import Alerts from './pages/Alerts';
import MapPage from './pages/MapPage';
import Forecast from './pages/Forecast';
import Search from './pages/Search';

export default function App() {
  return (
    <WebSocketProvider>
      <BrowserRouter>
        <Routes>
          <Route element={<Layout />}>
            <Route path="/" element={<Dashboard />} />
            <Route path="/explain" element={<SpikeExplainer />} />
            <Route path="/map" element={<MapPage />} />
            <Route path="/forecast" element={<Forecast />} />
            <Route path="/brief" element={<DailyBrief />} />
            <Route path="/screener" element={<Screener />} />
            <Route path="/alerts" element={<Alerts />} />
            <Route path="/search" element={<Search />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </WebSocketProvider>
  );
}

```

### backend/app/main.py

```python
"""EnergyX — FastAPI application entry point."""

import asyncio
import logging
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone

# Configure root logger so our app's log messages are visible
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from sqlalchemy import select, func, text

from app.config import get_settings
from app.database import init_db, async_session
from app.models.market_data import PriceRecord
from app.ingestion.scheduler import start_scheduler, stop_scheduler, ingest_iso_data
from app.api import market, explain, briefs, alerts, screener, chat, forecast, map as map_api, models as models_api, search
from app.api.ws import router as ws_router
from app.realtime.live_feed import live_feed_loop, stop_live_feed

settings = get_settings()
logger = logging.getLogger(__name__)

_live_feed_task = None

# Startup health tracking
_startup_status: dict[str, str] = {
    "db": "pending",
    "ingestion": "pending",
    "rag": "pending",
    "llm": "unchecked",
}


def _validate_config():
    """Log warnings for missing optional config so users know what to set."""
    if not settings.openai_api_key:
        logger.warning("⚠️  OPENAI_API_KEY not set — chat, spike explainer, and daily brief will fail")
        _startup_status["llm"] = "not_configured"
    else:
        _startup_status["llm"] = "configured"

    if settings.runpod_base_url:
        logger.info("RunPod self-hosted LLM configured: %s", settings.runpod_model)
    else:
        logger.info("RUNPOD_BASE_URL not set — self-hosted LLM disabled")

    if settings.elasticsearch_url:
        logger.info("Elasticsearch RAG configured: %s", settings.elasticsearch_url[:50])
    else:
        logger.info("ELASTICSEARCH_URL not set — Elasticsearch RAG disabled (ChromaDB only)")

    if not settings.slack_webhook_url:
        logger.info("SLACK_WEBHOOK_URL not set — Slack alerts disabled")
    if not settings.sendgrid_api_key:
        logger.info("SENDGRID_API_KEY not set — email alerts disabled")


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Startup: init DB + validate config + start services. Shutdown: cleanup."""
    global _live_feed_task

    # Validate configuration
    _validate_config()

    # Init DB
    await init_db()
    _startup_status["db"] = "ok"

    # Start scheduler
    start_scheduler()

    # Background tasks — errors are caught and logged, never crash startup
    asyncio.create_task(_initial_ingestion())

    # Start real-time WebSocket feed
    _live_feed_task = asyncio.create_task(live_feed_loop(interval_seconds=45))

    # Seed ChromaDB RAG store with historical events
    asyncio.create_task(_seed_rag())

    yield

    stop_live_feed()
    if _live_feed_task:
        _live_feed_task.cancel()
        try:
            await _live_feed_task
        except asyncio.CancelledError:
            pass
    stop_scheduler()


async def _seed_rag():
    """Initialize and seed both ChromaDB and Elasticsearch RAG stores."""
    rag_status = []

    # --- ChromaDB ---
    try:
        from app.rag.chroma_store import get_chroma_store
        store = get_chroma_store()
        if store:
            logger.info("ChromaDB RAG store initialized")
            rag_status.append("chromadb:ok")
        else:
            rag_status.append("chromadb:disabled")
    except Exception as e:
        logger.warning("ChromaDB init failed (non-fatal): %s", e)
        rag_status.append("chromadb:error")

    # --- Elasticsearch ---
    try:
        from app.rag.elasticsearch_store import get_rag_store
        es_store = get_rag_store()
        if es_store:
            await es_store.ensure_indices()
            rag_status.append("elasticsearch:ok")
        else:
            rag_status.append("elasticsearch:disabled")
    except Exception as e:
        logger.warning("Elasticsearch init failed (non-fatal): %s", e)
        rag_status.append("elasticsearch:error")

    _startup_status["rag"] = ", ".join(rag_status) if rag_status else "disabled"


async def _deduplicate_prices():
    """Remove duplicate price records (keeps the first by ID)."""
    try:
        async with async_session() as session:
            result = await session.execute(
                text("SELECT COUNT(*) FROM prices")
            )
            before = result.scalar()
            await session.execute(text("""
                DELETE FROM prices WHERE id NOT IN (
                    SELECT MIN(id) FROM prices
                    GROUP BY iso, location, timestamp, market
                )
            """))
            await session.commit()
            result = await session.execute(
                text("SELECT COUNT(*) FROM prices")
            )
            after = result.scalar()
        if before != after:
            logger.info("Deduplicated prices: %d -> %d records (removed %d duplicates)", before, after, before - after)
    except Exception as e:
        logger.warning("Price deduplication failed (non-fatal): %s", e)


async def _initial_ingestion():
    """Run first data ingestion — backfill 7 days if DB has insufficient history."""
    try:
        await _deduplicate_prices()

        async with async_session() as session:
            result = await session.execute(select(func.count(PriceRecord.id)))
            count = result.scalar()

            # Check date range coverage
            result = await session.execute(select(func.min(PriceRecord.timestamp)))
            oldest = result.scalar()

        now = datetime.now(timezone.utc)
        # Ensure both datetimes are comparable (SQLite stores naive UTC)
        if oldest and oldest.tzinfo is None:
            oldest = oldest.replace(tzinfo=timezone.utc)
        has_history = oldest and (now - oldest).days >= 6

        if count and count > 0 and has_history:
            logger.info("DB has %d records sp
[truncated — 5870 more characters]
```

### frontend/vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        ws: true,  // proxy WebSocket connections too
      },
    },
  },
})

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Plus+Jakarta+Sans:ital,opsz,wght@0,200..800;1,200..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet" />
    <title>EnergyX</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### backend/test_brief.py

```python
"""Quick smoke test for daily_brief module."""
import pandas as pd
from app.intelligence.daily_brief import (
    render_brief_html,
    _make_share_id,
    _build_fallback_brief,
    _gather_fuel_summary,
    _gather_load_summary,
)

# 1. Share ID generation
sid = _make_share_id("ERCOT", "2026-02-13")
print("Share ID:", sid)
assert len(sid) == 12

# 2. Fallback brief
brief = _build_fallback_brief(
    "ERCOT",
    "2026-02-13",
    [
        {
            "location": "HB_HOUSTON",
            "latest_price": 42.5,
            "mean_price": 38.2,
            "volatility": 12.3,
            "max_ramp": 8.1,
        }
    ],
    {
        "avg_price": 38.2,
        "max_price": 85.1,
        "min_price": 12.0,
        "total_spikes": 2,
        "top_volatility_location": "HB_HOUSTON",
        "peak_load_mw": 45000,
        "avg_load_mw": 38000,
    },
    {
        "available": True,
        "avg_mw": {"Solar": 5000, "Wind": 12000, "Gas": 20000},
        "pct_of_total": {"Solar": 13.5, "Wind": 32.4, "Gas": 54.1},
        "total_avg_mw": 37000,
    },
    {"available": True, "peak_mw": 45000, "avg_mw": 38000},
)
print("Fallback brief title:", brief["title"])
print("Key movers count:", len(brief["key_movers"]))
print("Risk indicators:", len(brief["risk_indicators"]))
assert "risk_indicators" in brief
assert "generation_summary" in brief
assert "load_summary" in brief

# 3. HTML rendering
brief["_meta"] = {
    "iso": "ERCOT",
    "date": "2026-02-13",
    "generated_at": "2026-02-14T10:00:00",
    "share_id": sid,
}
html = render_brief_html(brief)
print("HTML length:", len(html))
assert html.startswith("<!DOCTYPE")
assert "ERCOT" in html

# 4. Fuel summary with CAISO-style columns (should skip metadata cols)
caiso_df = pd.DataFrame(
    {
        "Time": ["2026-02-13 00:00", "2026-02-13 00:05"],
        "Interval Start": ["2026-02-13 00:00", "2026-02-13 00:05"],
        "Interval End": ["2026-02-13 00:05", "2026-02-13 00:10"],
        "Solar": [100, 200],
        "Wind": [1500, 1400],
        "Natural Gas": [5000, 5100],
    }
)
fuel_s = _gather_fuel_summary(caiso_df)
print("Fuel summary available:", fuel_s["available"])
assert "Interval Start" not in fuel_s.get("avg_mw", {})
assert "Solar" in fuel_s["avg_mw"]
print("Fuel avg_mw:", fuel_s["avg_mw"])

# 5. Load summary
load_df = pd.DataFrame(
    {
        "Time": pd.date_range("2026-02-13", periods=5, freq="5min"),
        "Load": [40000, 42000, 45000, 43000, 41000],
    }
)
load_s = _gather_load_summary(load_df)
print("Load summary:", load_s)
assert load_s["available"]
assert load_s["peak_mw"] == 45000

print("\nAll tests passed!")

```

[67 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]