# Project export: Orion

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: Cal Hacks 12.0
- Tagline: AI-powered wildfire operations dashboard with autonomous agents, live video streaming, and voice control—transforming emergency response through real-time intelligence.
- Devpost: https://devpost.com/software/orion-p5ayks
- GitHub: https://github.com/SupratikPanuganti/CalHacks
- Video: https://www.youtube.com/embed/MX_pJRwUJjU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Warp: Best Use of Warp; Vapi: Best Use of Vapi)
- Team: 4 GitHub contributor(s) — SupratikPanuganti (76 commits), AjDurga (41 commits), IndianaJonesR (24 commits), Raamia (5 commits)

## Devpost submission (written by the team)

### Inspiration

In California, 81% of residents (26M+) live with wildfire risk, and 2025 alone has seen 7,500+ fires that burned ~500,000 acres. In late-night ride-alongs and debriefs with emergency volunteers, one theme kept coming up: incident commanders are forced to stitch together radios, spreadsheets, texts, and maps while the fire grows by the minute. Orion was born to replace that chaos with a single, AI-powered command center—so responders can act faster, smarter, and safer.

### What it does

Orion is an AI wildfire operations dashboard that turns raw data into decisions: 🗺️ Live 3D Situational Awareness — Real-time fire incidents, wind-aware spread prediction, weather/AQI overlays, and embedded live video on the map. 🤖 Autonomous Agents — Background Python + Fetch.ai uAgents monitor incidents, compute risk, and continuously propose optimal routes for responders and evacuations. 🗣️ Voice Operations — VAPI-powered hands-free control: “Status of Pine Ridge? Dispatch two teams. Read me wind for the next hour.” 🚒 Dynamic Dispatch & Containment — One-click deploy from nearest station, animated units along routes, on-scene detection (Haversine), and visual containment progression. 🚨 Multi-Channel Alerts — Telegram/SMS notifications with formatted incident payloads, delivery confirmation, and retry logic. Outcome: a unified command center that cuts coordination time, reduces guesswork, and helps protect people and property when every second counts.

### How we built it

Frontend & UX Next.js 16 + React 18 + TypeScript for fast, type-safe UI Tailwind + Radix UI + shadcn for accessible, production-ready components Mapbox GL JS + react-map-gl for 3D terrain, custom layers, and route rendering Realtime & Data Supabase (Postgres + Realtime) for incidents, firestations, responders; sub-second sync via WebSockets Open-Meteo for weather/AQI (no key), Mapbox Directions for routing AI, Agents & Voice Python uAgents (Fetch.ai): monitoring agent (risk scoring, anomaly detection) + route agent VAPI (@vapi-ai/web): domain-tuned prompts, low-latency voice queries & responses Comms & Integrations Composio + Telegram Bot API for one-click alerts, message IDs, retries Fire Spread Modeling Custom multi-octave noise + wind vectors to generate organic, non-circular boundaries and time-based growth. Deployed with Vercel/AWS Amplify; strict ESLint/TypeScript rules, cleanup on unmount, and debounced updates to keep the UI crisp.

### Challenges we ran into

Realistic fire behavior without jank: Getting organic, wind-influenced edges that don’t “flicker” or become perfect circles required multi-octave noise, normalization, and careful animation timing. Keeping everything in sync: Incidents, responders, routes, weather panels, and voice state had to update in lock-step. Memory leaks were a risk—solved with disciplined subscription cleanup and state batching. Smooth responder movement + arrival detection: Interpolating along polyline routes while computing precise on-scene thresholds (Haversine) and status transitions. Voice in crisis contexts: VAPI needed wildfire terminology, context injection (active incidents, weather), and graceful fallback when mic/connection issues occurred. Video performance in map popups: Ensured codecs/fallbacks didn’t tank FPS; isolated rendering paths.

### Accomplishments we're proud of

✅ A complete, working workflow: detection → analysis → dispatch → alerts → containment visualization. ✅ Sub-second realtime across clients with Supabase Realtime—and zero critical leaks. ✅ Agent ecosystem that actually runs unattended (monitoring + routing) with actionable recommendations. ✅ Voice-first operations that meaningfully reduce clicks in high-stress moments. ✅ Designed for scale: 25+ active incidents, many concurrent users, 60 FPS animations on commodity hardware.

### What we learned

Geospatial math is everything: Haversine distance, bearings, and interpolation underpin user trust. Realtime demands discipline: WebSocket lifecycles, debouncing, optimistic updates, and teardown patterns are non-negotiable. Voice shines when hands are busy: In command roles, conversational ops beat menus—if latency stays <2s and context is accurate. Agents ≠ background tasks: Orchestration, retries, and human-in-the-loop UX are product features, not just infra.

### What's next

🛰️ NASA FIRMS satellite ingestion for earlier detection 📱 Native iOS/Android apps for field teams with offline sync 🗺️ Automatic evacuation zone mapping + geofenced public alerts ⚡ Predictive resource staging (AI forecasts for pre-positioning crews/equipment) 👥 Multi-agency roles & audit trails (CAL FIRE, local, federal) 📊 Historical analytics: response times, allocation efficiency, and after-action insights Orion’s mission: put actionable intelligence in the hands of first responders—when it matters most.

## README (from the GitHub repository)

# 🔥 Orion - Wildfire Operations Dashboard

<div align="center">

![Orion Logo](public/placeholder-logo.png)

**AI-Powered Wildfire Command Center with Autonomous Agents, Live Video Streaming, and Real-Time Intelligence**

_Built at CalHacks • Transforming Emergency Response through AI_

[![Next.js](https://img.shields.io/badge/Next.js-16.0-black?style=for-the-badge&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org/)
[![Supabase](https://img.shields.io/badge/Supabase-Realtime-green?style=for-the-badge&logo=supabase)](https://supabase.com/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.0-38bdf8?style=for-the-badge&logo=tailwind-css)](https://tailwindcss.com/)

### 🔥 **Live Video Intelligence** • 🤖 **Autonomous AI Agents** • 🗣️ **Voice Operations** • 🗺️ **3D Fire Simulation** • 🚨 **Smart Alerts**

[🚀 Features](#-features) • [🎬 User Flow](#-user-flow) • [🛠️ Installation](#️-installation) • [⚙️ Configuration](#️-configuration) • [🏗️ Architecture](#️-architecture)

</div>

---

## 📋 Table of Contents

- [Feature Highlights](#-feature-highlights)
- [Overview](#-overview)
  - [Product Purpose](#-product-purpose)
  - [Key Capabilities](#-key-capabilities)
- [Features](#-features)
  - [Interactive 3D Map](#️-interactive-3d-map)
  - [AI Voice Assistant](#-ai-voice-assistant)
  - [Environmental Conditions](#️-environmental-conditions)
  - [Emergency Alert System](#-emergency-alert-system)
  - [Incident Management](#-incident-management)
  - [Video Preview System](#-video-preview-system)
  - [Autonomous AI Agent System](#-autonomous-ai-agent-system)
  - [Modern UI/UX](#-modern-uiux)
- [User Flow](#-user-flow)
  - [Complete Operational Workflow](#complete-operational-workflow)
  - [Real-Time Synchronization Flow](#-real-time-synchronization-flow)
  - [Typical Use Cases](#-typical-use-cases)
- [Technology Stack](#️-technology-stack)
- [Installation](#️-installation)
- [Configuration](#️-configuration)
- [Usage](#-usage)
- [Project Structure](#-project-structure)
- [Architecture](#️-architecture)
- [API Reference](#-api-reference)
- [Database Schema](#-database-schema)
- [Components](#-components)
- [Real-Time Features](#-real-time-features)
- [Styling & Theming](#-styling--theming)
- [Testing](#-testing)
- [Performance Optimizations](#-performance-optimizations)
- [Security Considerations](#-security-considerations)
- [Future Enhancements](#-future-enhancements)
- [Troubleshooting](#-troubleshooting)
- [Contributing](#-contributing)
- [License](#-license)
- [Team](#-team)
- [Acknowledgments](#-acknowledgments)
- [Resources](#-resources)

---

## 🎯 Feature Highlights

<div align="center">

### **🔥 What Makes Orion Special**

| Feature                       | Description                                                                | Technology                               |
| ----------------------------- | -------------------------------------------------------------------------- | ---------------------------------------- |
| 🤖 **Autonomous AI Agents**   | Background Python + Fetch.ai agents monitor fires and optimize routes 24/7 | Python, Fetch.ai, Real-time APIs         |
| 🗣️ **Voice AI Operations**    | Hands-free incident management with conversational AI assistant            | VAPI, Natural Language Processing        |
| 📹 **Live Video Streaming**   | Real-time fire footage embedded directly in map interface                  | Embedded video players, MP4 streaming    |
| 🗺️ **3D Fire Visualization**  | Physics-based fire spread with wind-influenced directional growth          | Mapbox GL, Multi-octave noise algorithms |
| 🚨 **Multi-Channel Alerts**   | Automated SMS + Telegram emergency alerts with intelligent routing         | Telegram Bot API, Composio               |
| 📊 **Real-Time Intelligence** | Live weather, air quality, and environmental risk assessment               | Open-Meteo API, Supabase Realtime        |
| 🛣️ **Dynamic Rerouting**      | AI-calculated evacuation routes that adapt to fire spread patterns         | Mapbox Directions API, AI optimization   |
| ⚡ **Instant Sync**           | Sub-second database updates across all connected clients                   | Supabase Realtime subscriptions          |

</div>

---

## 🌟 Overview

**Orion** is a next-generation, AI-powered wildfire operations command center designed for emergency response teams, fire departments, and incident commanders. Built during CalHacks, Orion transforms wildfire response through real-time intelligence, autonomous decision-making, and seamless human-AI collaboration.

### 🎯 Product Purpose

Orion addresses the critical challenge of wildfire response coordination by providing a **unified command platform** that combines:

- **Real-time situational awareness** across all active fires
- **AI-driven decision support** for resource deployment and routing
- **Voice-activated operations** for hands-free incident management
- **Automated emergency communications** via multiple channels
- **Live video intelligence** from fire locations
- **Predictive fire behavior modeling** with environmental data integration

**Mission**: Reduce emergency response times, optimize resource allocation, and save lives through intelligent automation and real-time coordination.

### 🚀 Key Capabilities

#### 🔥 **Intelligent Fire Operations**

- **Real-Time Incident Tracking**: Monitor active wildfires with live database updates and automatic synchronization
- **AI Agent Fire Monitoring**: Autonomous Python agent continuously monitors fire states and recommends optimal response strategies
- **Dynamic Fire Spread Visualization**: Physics-based fire simulation with wind, terrain, and fuel modeling
- **Live Video Streaming**: Real-time video feeds from active fire locations embedded in map interface

#### 🤖 **Autonomous AI Systems**

- **Agentic Voice AI**: Conversational AI assistant powered by VAPI for hands-free operations
- **AI Route Optimization**: Fetch.ai agents automatically calculate and recommend evacuation routes and fire station deployment paths
- **Intelligent Alert System**: AI-powered SMS/Telegram alerts with contextual fire information
- **Predictive Risk Assessment**: Real-time fire danger analysis based on environmental conditions

#### 🗺️ **Advanced Geospatial Intelligence**

- **3D Interactive Mapping**: Mapbox GL terrain visualization with realistic elevation and buildings
- **Live Route Planning**: Dynamic rerouting based on fire spread, road closures, and risk zones
- **Evacuation Zone Mapping**: Automatic generation of evacuation perimeters
- **Fire Station Network**: Nearest station identification with optimal response routing

#### 📊 **Real-Time Environmental Monitoring**

- **Weather Integration**: Live temperature, humidity, wind speed/direction from Open-Meteo API
- **Air Quality Tracking**: PM2.5, PM10, and AQI monitoring for health advisories
- **Fire Risk Scoring**: Dynamic fire danger index based on multiple environmental factors
- **Visibility Tracking**: Real-time visibility conditions for aviation and ground operations

#### 🚨 **Multi-Channel Emergency Communications**

- **SMS Emergency Alerts**: Automated SMS dispatch to emergency contacts and responders
- **Telegram Integration**: Instant messaging to command channels with formatted incident reports
- **Voice Notifications**: AI-generated voice alerts for critical situations
- **Alert Tracking**: Message delivery confirmation and status monitoring

---

## ✨ Features

### 🗺️ Interactive 3D Map

- **Mapbox GL 3D Terrain Visualization**

  - 3D buildings and terrain with realistic elevation
  - Dynamic day/night sky rendering
  - Smooth camera animations and transitions
  - Pan, zoom, pitch, and rotate controls

- **Fire Incident Markers**

  - Color-coded by risk level (Critical, High, Medium, Low)
  - Animated pulse effects for active fires
  - Particl

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 93 recognized source files, 495 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AWS (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (100 of 100)

```
.gitignore
amplify.yml
app/api/dispatch-responder/route.ts
app/api/fire-state/route.ts
app/api/send-emergency-alert/route.ts
app/api/update-routes/route.ts
app/api/vapi-webhook/route.ts
app/api/weather/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
app/test-video/page.tsx
components.json
components/california-map.tsx
components/IncidentsList.tsx
components/theme-provider.tsx
components/ui/accordion.tsx
components/ui/alert-dialog.tsx
components/ui/alert.tsx
components/ui/aspect-ratio.tsx
components/ui/avatar.tsx
components/ui/badge.tsx
components/ui/breadcrumb.tsx
components/ui/button-group.tsx
components/ui/button.tsx
components/ui/calendar.tsx
components/ui/card.tsx
components/ui/carousel.tsx
components/ui/chart.tsx
components/ui/checkbox.tsx
components/ui/collapsible.tsx
components/ui/command.tsx
components/ui/context-menu.tsx
components/ui/dialog.tsx
components/ui/drawer.tsx
components/ui/dropdown-menu.tsx
components/ui/empty.tsx
components/ui/field.tsx
components/ui/form.tsx
components/ui/hover-card.tsx
components/ui/input-group.tsx
components/ui/input-otp.tsx
components/ui/input.tsx
components/ui/item.tsx
components/ui/kbd.tsx
components/ui/label.tsx
components/ui/menubar.tsx
components/ui/navigation-menu.tsx
components/ui/pagination.tsx
components/ui/popover.tsx
components/ui/progress.tsx
components/ui/radio-group.tsx
components/ui/resizable.tsx
components/ui/scroll-area.tsx
components/ui/select.tsx
components/ui/separator.tsx
components/ui/sheet.tsx
components/ui/sidebar.tsx
components/ui/skeleton.tsx
components/ui/slider.tsx
components/ui/sonner.tsx
components/ui/spinner.tsx
components/ui/switch.tsx
components/ui/table.tsx
components/ui/tabs.tsx
components/ui/textarea.tsx
components/ui/toast.tsx
components/ui/toaster.tsx
components/ui/toggle-group.tsx
components/ui/toggle.tsx
components/ui/tooltip.tsx
components/ui/use-mobile.tsx
components/ui/use-toast.ts
components/VideoPreview.tsx
components/voice-assistant.tsx
hooks/use-mobile.ts
hooks/use-toast.ts
hooks/use-vapi.ts
hooks/use-weather.ts
lib/composio-telegram-service.ts
lib/fetch-agent.ts
lib/supabase-client.ts
lib/utils.ts
lib/video-mapping.ts
next-env.d.ts
next.config.mjs
package.json
postcss.config.mjs
presentation.html
public/videos/README.md
python-agent/fire_monitor_agent.py
python-agent/requirements.txt
python-agent/start.sh
python-agent/test_connection.py
README.md
scripts/check-responders.ts
scripts/start-agent.ts
scripts/test-weather-api.ts
styles/globals.css
tsconfig.json
```

### Dependencies

- package.json: @composio/core@^0.2.0, @hookform/resolvers@^3.10.0, @radix-ui/react-accordion@1.2.2, @radix-ui/react-alert-dialog@1.1.4, @radix-ui/react-aspect-ratio@1.1.1, @radix-ui/react-avatar@1.1.2, @radix-ui/react-checkbox@1.1.3, @radix-ui/react-collapsible@1.1.2, @radix-ui/react-context-menu@2.2.4, @radix-ui/react-dialog@1.1.4, @radix-ui/react-dropdown-menu@2.1.4, @radix-ui/react-hover-card@1.1.4, @radix-ui/react-label@2.1.1, @radix-ui/react-menubar@1.1.4, @radix-ui/react-navigation-menu@1.2.3, @radix-ui/react-popover@1.1.4, @radix-ui/react-progress@1.1.1, @radix-ui/react-radio-group@1.2.2, @radix-ui/react-scroll-area@1.2.2, @radix-ui/react-select@2.1.4, @radix-ui/react-separator@1.1.1, @radix-ui/react-slider@1.2.2, @radix-ui/react-slot@1.1.1, @radix-ui/react-switch@1.1.2, @radix-ui/react-tabs@1.1.2, @radix-ui/react-toast@1.2.4, @radix-ui/react-toggle@1.1.1, @radix-ui/react-toggle-group@1.1.1, @radix-ui/react-tooltip@1.1.6, @supabase/supabase-js@^2.76.1, @tailwindcss/postcss@^4.1.9, @types/node@^22, @types/react@^19, @types/react-dom@^19, @vapi-ai/web@^2.5.0, @vercel/analytics@1.3.1, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.0.4, composio-core@^0.5.39, date-fns@4.1.0, embla-carousel-react@8.5.1, input-otp@1.4.1, lightningcss@^1.30.2, lucide-react@^0.454.0, mapbox-gl@^2.15.0, next@16.0.0, next-themes@^0.4.6, postcss@^8.5, react@^18.3.1, react-day-picker@9.8.0, react-dom@^18.3.1, react-hook-form@^7.60.0, react-map-gl@^7.1.7, react-resizable-panels@^2.1.7, recharts@2.15.4, sonner@^1.7.4, tailwind-merge@^2.5.5, tailwindcss@^4.1.9, tailwindcss-animate@^1.0.7, tw-animate-css@1.3.3, typescript@^5, vaul@^0.9.9, zod@3.25.76
- python-agent/requirements.txt: python-dotenv@>=1.0.0, requests@>=2.31.0, uagents@>=0.12.0

### Recent commits (newest first)

- Delete .env.local
- Merge supratik branch: Add weather API integration and presentation
- Add weather API integration and presentation
- Merge pull request #29 from SupratikPanuganti/supratik
- Fix firefighting logic and responder tracking
- Merge pull request #28 from SupratikPanuganti/supratik
- Merge branch 'main' into supratik
- Add comprehensive README and improve map UX: hide fire labels until clicked
- Merge pull request #27 from SupratikPanuganti/rohit#2
- Fetch.AI Integration
- Merge pull request #26 from SupratikPanuganti/feat/logo-flame
- Header: replace pulse icon with flame outline for Orion logo
- Resolve merge conflict in app/page.tsx
- Merge pull request #25 from SupratikPanuganti/supratik
- Update welcome screen: change icon to robot and add feature bullets
- Merge remote-tracking branch 'origin/main' into rohit#2
- fixed merging errors and updated new environment
- Merge remote-tracking branch 'origin/main' into rohit#2
- FireUpdates
- Merge pull request #24 from SupratikPanuganti/supratik

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

### package.json

```
{
  "name": "Orion",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "lint": "eslint .",
    "start": "next start",
    "agent": "ts-node scripts/start-agent.ts",
    "test:weather": "npx ts-node scripts/test-weather-api.ts"
  },
  "dependencies": {
    "@composio/core": "^0.2.0",
    "@hookform/resolvers": "^3.10.0",
    "@radix-ui/react-accordion": "1.2.2",
    "@radix-ui/react-alert-dialog": "1.1.4",
    "@radix-ui/react-aspect-ratio": "1.1.1",
    "@radix-ui/react-avatar": "1.1.2",
    "@radix-ui/react-checkbox": "1.1.3",
    "@radix-ui/react-collapsible": "1.1.2",
    "@radix-ui/react-context-menu": "2.2.4",
    "@radix-ui/react-dialog": "1.1.4",
    "@radix-ui/react-dropdown-menu": "2.1.4",
    "@radix-ui/react-hover-card": "1.1.4",
    "@radix-ui/react-label": "2.1.1",
    "@radix-ui/react-menubar": "1.1.4",
    "@radix-ui/react-navigation-menu": "1.2.3",
    "@radix-ui/react-popover": "1.1.4",
    "@radix-ui/react-progress": "1.1.1",
    "@radix-ui/react-radio-group": "1.2.2",
    "@radix-ui/react-scroll-area": "1.2.2",
    "@radix-ui/react-select": "2.1.4",
    "@radix-ui/react-separator": "1.1.1",
    "@radix-ui/react-slider": "1.2.2",
    "@radix-ui/react-slot": "1.1.1",
    "@radix-ui/react-switch": "1.1.2",
    "@radix-ui/react-tabs": "1.1.2",
    "@radix-ui/react-toast": "1.2.4",
    "@radix-ui/react-toggle": "1.1.1",
    "@radix-ui/react-toggle-group": "1.1.1",
    "@radix-ui/react-tooltip": "1.1.6",
    "@supabase/supabase-js": "^2.76.1",
    "@vapi-ai/web": "^2.5.0",
    "@vercel/analytics": "1.3.1",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.0.4",
    "composio-core": "^0.5.39",
    "date-fns": "4.1.0",
    "embla-carousel-react": "8.5.1",
    "input-otp": "1.4.1",
    "lucide-react": "^0.454.0",
    "mapbox-gl": "^2.15.0",
    "next": "16.0.0",
    "next-themes": "^0.4.6",
    "react": "^18.3.1",
    "react-day-picker": "9.8.0",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.60.0",
    "react-map-gl": "^7.1.7",
    "react-resizable-panels": "^2.1.7",
    "recharts": "2.15.4",
    "sonner": "^1.7.4",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.9",
    "zod": "3.25.76"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4.1.9",
    "@types/node": "^22",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "lightningcss": "^1.30.2",
    "postcss": "^8.5",
    "tailwindcss": "^4.1.9",
    "tw-animate-css": "1.3.3",
    "typescript": "^5"
  }
}

```

### python-agent/requirements.txt

```
uagents>=0.12.0
requests>=2.31.0
python-dotenv>=1.0.0


```

### app/layout.tsx

```typescript
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { Analytics } from '@vercel/analytics/next'
import './globals.css'

const inter = Inter({ 
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
  variable: "--font-inter"
});

export const metadata: Metadata = {
  title: 'Orion Wildfire Operations Dashboard',
  description: 'Next-generation command center interface for wildfire response operations',
  generator: 'v0.app',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body className={`${inter.variable} font-sans antialiased`}>
        {children}
        <Analytics />
      </body>
    </html>
  )
}

```

### app/test-video/page.tsx

```typescript
'use client';

export default function TestVideoPage() {
  return (
    <div className="p-8 bg-black min-h-screen">
      <h1 className="text-2xl text-white mb-6">Video Test Page</h1>
      
      <div className="space-y-8">
        <div>
          <h2 className="text-xl text-white mb-2">Pine Ridge Fire Video</h2>
          <video 
            controls 
            className="w-full max-w-2xl"
            src="/videos/pine-ridge-fire.mp4"
          >
            Your browser does not support the video tag.
          </video>
          <p className="text-white mt-2">Path: /videos/pine-ridge-fire.mp4</p>
        </div>

        <div>
          <h2 className="text-xl text-white mb-2">Redwood Valley Fire Video</h2>
          <video 
            controls 
            className="w-full max-w-2xl"
            src="/videos/redwood-valley-fire.mp4"
          >
            Your browser does not support the video tag.
          </video>
          <p className="text-white mt-2">Path: /videos/redwood-valley-fire.mp4</p>
        </div>

        <div>
          <h2 className="text-xl text-white mb-2">Direct Link Tests</h2>
          <div className="space-y-2">
            <a 
              href="/videos/pine-ridge-fire.mp4" 
              target="_blank"
              className="block text-blue-400 hover:underline"
            >
              Open Pine Ridge Fire video in new tab
            </a>
            <a 
              href="/videos/redwood-valley-fire.mp4" 
              target="_blank"
              className="block text-blue-400 hover:underline"
            >
              Open Redwood Valley Fire video in new tab
            </a>
          </div>
        </div>
      </div>
    </div>
  );
}


```

### app/page.tsx

```typescript
"use client"

import { useState, useEffect, useCallback } from "react"
import { Play, SkipBack, SkipForward, Wind, Activity, AlertTriangle, Mic, Volume2, ChevronLeft, ChevronRight, PanelLeft, PanelRight, MicOff, ChevronUp, Minimize2, Bot, Flame } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import CaliforniaMap from "@/components/california-map"
import { useVapi } from "@/hooks/use-vapi"
import { useWeather } from "@/hooks/use-weather"
import IncidentsList from '@/components/IncidentsList'
import { FireIncident } from '@/lib/composio-telegram-service'

// Helper functions for severity-based colors
function getHumidityColor(humidity: number): string {
  if (humidity < 20) return '#FF4444'; // Critical - very dry, high fire risk
  if (humidity < 30) return '#FF6B00'; // High risk
  if (humidity < 50) return '#FFA800'; // Moderate
  return '#00C2FF'; // Good
}

function getTemperatureColor(temp: number): string {
  if (temp >= 100) return '#FF4444'; // Extreme heat
  if (temp >= 90) return '#FF6B00'; // Very hot
  if (temp >= 80) return '#FFA800'; // Hot
  if (temp >= 70) return '#00C2FF'; // Warm
  if (temp >= 50) return '#00FFB3'; // Mild
  return '#00C2FF'; // Cool
}

function getVisibilityColor(visibilityMiles: number): string {
  if (visibilityMiles < 1) return '#FF4444'; // Poor
  if (visibilityMiles < 3) return '#FF6B00'; // Moderate
  if (visibilityMiles < 6) return '#FFA800'; // Fair
  return '#00C2FF'; // Good
}

function getVisibilityLabel(visibilityMiles: number): string {
  if (visibilityMiles < 1) return 'Poor';
  if (visibilityMiles < 3) return 'Moderate';
  if (visibilityMiles < 6) return 'Fair';
  return 'Good';
}

function getAirQualityColor(aqi: number): string {
  if (aqi >= 201) return '#8B1A1A'; // Very Unhealthy (Purple/Maroon)
  if (aqi >= 151) return '#FF4444'; // Unhealthy (Red)
  if (aqi >= 101) return '#FF6B00'; // Unhealthy for Sensitive Groups (Orange)
  if (aqi >= 51) return '#FFA800'; // Moderate (Yellow)
  return '#00FFB3'; // Good (Green)
}

function getAirQualityLabel(aqi: number): string {
  if (aqi >= 201) return 'Very Unhealthy';
  if (aqi >= 151) return 'Unhealthy';
  if (aqi >= 101) return 'Unhealthy (Sensitive)';
  if (aqi >= 51) return 'Moderate';
  return 'Good';
}

export default function OrionDashboard() {
  const [leftSidebarCollapsed, setLeftSidebarCollapsed] = useState(false)
  const [rightSidebarCollapsed, setRightSidebarCollapsed] = useState(false)
  const [aiAssistantCollapsed, setAiAssistantCollapsed] = useState(false)
  const [environmentalConditionsCollapsed, setEnvironmentalConditionsCollapsed] = useState(false)
  const [activeIncidentsCollapsed, setActiveIncidentsCollapsed] = useState(false)
  const [incidentCount, setIncidentCount] = useState(0)
  const [currentIncidents, setCurrentIncidents] = useState<FireIncident[]>([])
  const [selectedIncident, setSelectedIncident] = useState<FireIncident | null>(null)
  const [alertSending, setAlertSending] = useState(false)
  const [alertStatus, setAlertStatus] = useState<string | null>(null)
  const [responderStats, setResponderStats] = useState<{ available: number; dispatched: number; active: number; eta?: string }>({ available: 0, dispatched: 0, active: 0 })
  const [allResponderStats, setAllResponderStats] = useState<{ available: number; dispatched: number; active: number }>({ available: 0, dispatched: 0, active: 0 })
  const [dispatchSending, setDispatchSending] = useState(false)
  const [refreshResponderTrigger, setRefreshResponderTrigger] = useState(0)
  
  // VAPI voice integration
  const { isSessionActive, isSpeaking, messages: vapiMessages, volumeLevel, start, stop, receiveAgentMessage } = useVapi()
  
  // Map center tracking for weather
  const [mapCenter, setMapCenter] = useState<{ lat: number; lon: number } | null>(null)
  const { weather, loading: weatherLoading } = useWeather(mapCenter?.lat ?? null, mapCenter?.lon ?? null)
  
  // Poll for AI agent messages
  useEffect(() => {
    let lastTimestamp: string | null = null;
    
    const pollAgentMessages = async () => {
      try {
        const url = lastTimestamp 
          ? `/api/vapi-webhook?since=${encodeURIComponent(lastTimestamp)}`
          : '/api/vapi-webhook';
        
        const response = await fetch(url);
        const data = await response.json();
        
        if (data.messages && data.messages.length > 0) {
          // Process each new message
          data.messages.forEach((msg: any) => {
            receiveAgentMessage({
              action: msg.action,
              message: msg.message,
              data: msg.data
            });
          });
          
          // Update last timestamp
          lastTimestamp = data.latest_timestamp;
        }
      } catch (error) {
        console.error('Error polling agent messages:', error);
      }
    };
    
    // Poll every 5 seconds
    const interval = setInterval(pollAgentMessages, 5000);
    pollAgentMessages(); // Initial poll
    
    return () => clearInterval(interval);
  }, [receiveAgentMessage])
  
  // Map navigation function
  const [navigateToIncident, setNavigateToIncident] = useState<((incident: any) => void) | null>(null)
  
  // Map ready callback
  const handleMapReady = (navFunction: any) => {
    setNavigateToIncident(() => navFunction);
  };

  // Map movement callback
  const handleMapMove = (center: { lat: number; lon: number }) => {
    setMapCenter(center);
  };
  
  const handleVoiceToggle = () => {
    if (isSessionActive) {
      stop()
    } else {
      start()
    }
  }

  const handleIncidentSelect = (incident: any) => {
    if (!incident) {
      console.warn('No incident provided to handleIncidentSelect');
      return;
    }
    
    // Convert to FireIncident format with last_update
    const fireIncident: FireIncident = {
      id: typeof incident.id === 'string' ? incident.id : String(incident.id || ''),
      name: incident.name || null,
      ri
[truncated — 33400 more characters]
```

### app/api/send-emergency-alert/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { sendEmergencyAlertViaTelegram, FireIncident } from '@/lib/composio-telegram-service';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { incident, customMessage } = body;

    // Validate required fields
    if (!incident) {
      return NextResponse.json(
        { success: false, error: 'Incident data is required' },
        { status: 400 }
      );
    }

    // Validate incident structure
    const requiredFields = ['id', 'name', 'risk', 'lat', 'lon', 'last_update'];
    const missingFields = requiredFields.filter(field => !incident[field]);
    
    if (missingFields.length > 0) {
      return NextResponse.json(
        { 
          success: false, 
          error: `Missing required fields: ${missingFields.join(', ')}` 
        },
        { status: 400 }
      );
    }

    // Send the emergency alert via Composio + Telegram
    const result = await sendEmergencyAlertViaTelegram(incident as FireIncident, customMessage);

    if (result.success) {
      return NextResponse.json({
        success: true,
        messageSid: result.messageId,
        message: 'Emergency alert sent successfully via Telegram'
      });
    } else {
      return NextResponse.json(
        { 
          success: false, 
          error: result.error || 'Failed to send emergency alert' 
        },
        { status: 500 }
      );
    }
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json(
      { 
        success: false, 
        error: 'Internal server error' 
      },
      { status: 500 }
    );
  }
}

// Handle unsupported methods
export async function GET() {
  return NextResponse.json(
    { error: 'Method not allowed. Use POST to send emergency alerts.' },
    { status: 405 }
  );
}

```

### app/api/vapi-webhook/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';

// In-memory store for agent messages (for SSE streaming)
// In production, use Redis or similar
let agentMessages: Array<{ action: string; message: string; timestamp: string; data?: any }> = [];
const MAX_MESSAGES = 100;

// POST: Receive messages from AI agent
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { action, message, data } = body;

    if (!action || !message) {
      return NextResponse.json(
        { error: 'Missing required fields: action and message are required' },
        { status: 400 }
      );
    }

    // Store message with timestamp
    const agentMessage = {
      action,
      message,
      data,
      timestamp: new Date().toISOString()
    };

    agentMessages.push(agentMessage);
    
    // Keep only last 100 messages
    if (agentMessages.length > MAX_MESSAGES) {
      agentMessages = agentMessages.slice(-MAX_MESSAGES);
    }

    console.log(`[VAPI Webhook] Received ${action}: ${message}`);

    return NextResponse.json({
      success: true,
      message: 'Agent message received',
      queued: agentMessages.length
    });
  } catch (error) {
    console.error('VAPI webhook error:', error);
    return NextResponse.json(
      { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}

// GET: Retrieve recent messages (for polling)
export async function GET(request: NextRequest) {
  try {
    const url = new URL(request.url);
    const since = url.searchParams.get('since');
    
    let messages = agentMessages;
    
    // Filter by timestamp if provided
    if (since) {
      const sinceDate = new Date(since);
      messages = messages.filter(msg => new Date(msg.timestamp) > sinceDate);
    }

    return NextResponse.json({
      messages,
      count: messages.length,
      latest_timestamp: messages.length > 0 ? messages[messages.length - 1].timestamp : null
    });
  } catch (error) {
    console.error('Error fetching VAPI messages:', error);
    return NextResponse.json(
      { error: 'Failed to fetch messages', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}


```

### app/api/update-routes/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { supabase } from '@/lib/supabase-client';

// GET: Retrieve latest route updates
export async function GET(request: NextRequest) {
  try {
    // Get route updates from last 5 minutes
    const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
    
    const { data: routeUpdates, error: routeError } = await supabase
      .from('route_updates')
      .select('*')
      .gte('created_at', fiveMinutesAgo)
      .order('created_at', { ascending: false });

    const { data: evacuations, error: evacError } = await supabase
      .from('evacuation_zones')
      .select('*')
      .gte('recommended_at', fiveMinutesAgo)
      .order('recommended_at', { ascending: false });

    return NextResponse.json({
      routes: routeUpdates || [],
      evacuations: evacuations || [],
      timestamp: new Date().toISOString()
    });
  } catch (error) {
    console.error('Error fetching route updates:', error);
    return NextResponse.json(
      { error: 'Failed to fetch routes', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}

// POST: Create new route update from AI agent
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { station_id, new_route, reason, original_route, risk_score } = body;

    // Validate required fields
    if (!station_id || !new_route) {
      return NextResponse.json(
        { error: 'Missing required fields: station_id and new_route are required' },
        { status: 400 }
      );
    }

    // Insert route update into Supabase
    const { data, error } = await supabase
      .from('route_updates')
      .insert({
        station_id,
        original_route: original_route || null,
        new_route,
        reason: reason || 'AI-recommended route adjustment',
        risk_score: risk_score || null
      })
      .select()
      .single();

    if (error) {
      return NextResponse.json(
        { error: 'Failed to save route update', details: error.message },
        { status: 500 }
      );
    }

    // Notify VAPI about the route update
    try {
      await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/vapi-webhook`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'route_update',
          message: reason || `Route updated for station ${station_id}`,
          data: { station_id, route: new_route }
        })
      });
    } catch (vapiError) {
      console.warn('Failed to notify VAPI:', vapiError);
      // Don't fail the route update if VAPI notification fails
    }

    return NextResponse.json({
      success: true,
      route_update: data,
      message: 'Route update saved successfully'
    });
  } catch (error) {
    console.error('Error creating route update:', error);
    return NextResponse.json(
      { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}

// POST to /evacuation endpoint: Create evacuation zone recommendation
export async function PUT(request: NextRequest) {
  try {
    const body = await request.json();
    const { fire_id, zone_name, polygon } = body;

    if (!fire_id || !polygon) {
      return NextResponse.json(
        { error: 'Missing required fields: fire_id and polygon are required' },
        { status: 400 }
      );
    }

    const { data, error } = await supabase
      .from('evacuation_zones')
      .insert({
        fire_id,
        zone_name,
        polygon
      })
      .select()
      .single();

    if (error) {
      return NextResponse.json(
        { error: 'Failed to save evacuation zone', details: error.message },
        { status: 500 }
      );
    }

    // Notify VAPI
    try {
      await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/vapi-webhook`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'evacuation',
          message: `Evacuation recommended for ${zone_name || 'affected area'}`,
          data: { fire_id, zone: polygon }
        })
      });
    } catch (vapiError) {
      console.warn('Failed to notify VAPI:', vapiError);
    }

    return NextResponse.json({
      success: true,
      evacuation_zone: data,
      message: 'Evacuation zone created successfully'
    });
  } catch (error) {
    console.error('Error creating evacuation zone:', error);
    return NextResponse.json(
      { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}


```

### app/api/fire-state/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { supabase } from '@/lib/supabase-client';

export async function GET(request: NextRequest) {
  try {
    // Fetch active fire incidents from Supabase
    const { data: incidents, error: incidentsError } = await supabase
      .from('incidents')
      .select('*')
      .eq('status', 'active')
      .order('last_update', { ascending: false });

    if (incidentsError) {
      return NextResponse.json(
        { error: 'Failed to fetch incidents', details: incidentsError.message },
        { status: 500 }
      );
    }

    // Fetch firestations
    const { data: firestations, error: firestationsError } = await supabase
      .from('firestations')
      .select('*')
      .order('id');

    if (firestationsError) {
      return NextResponse.json(
        { error: 'Failed to fetch firestations', details: firestationsError.message },
        { status: 500 }
      );
    }

    // Fetch responders to check if any are on scene for each fire
    const { data: allResponders } = await supabase
      .from('responders')
      .select('incident_id, status')
      .in('status', ['on_scene']);

    // Create a map of incident_id -> number of responders on scene
    const respondersOnSceneMap = new Map<string, number>();
    if (allResponders) {
      allResponders.forEach((r: any) => {
        if (r.incident_id) {
          const count = respondersOnSceneMap.get(String(r.incident_id)) || 0;
          respondersOnSceneMap.set(String(r.incident_id), count + 1);
        }
      });
    }

    // Format fire data with estimated polygon and growth rate
    const fires = (incidents || []).map((incident: any) => {
      // Estimate fire radius based on time since start (simplified)
      const startTime = new Date(incident.start_time).getTime();
      const now = Date.now();
      const elapsedMinutes = (now - startTime) / (1000 * 60);
      
      // Growth rate based on risk level
      const growthRates = {
        'critical': 50, // meters per minute
        'high': 35,
        'medium': 20,
        'low': 10
      };
      
      const growthRate = growthRates[incident.risk as keyof typeof growthRates] || 20;
      
      // Check if responders are on scene
      const respondersOnScene = respondersOnSceneMap.get(String(incident.id)) || 0;
      
      // Calculate base radius (what it grew to before responders arrived)
      let estimatedRadius: number;
      
      if (respondersOnScene > 0) {
        // Responders are on scene - fire should stop growing and start shrinking
        // Use containment percentage to calculate shrinkage
        const maxRadius = Math.min(elapsedMinutes * growthRate, 5000);
        const shrinkFactor = 1 - (incident.containment / 100) * 0.95; // Shrink up to 95%
        estimatedRadius = maxRadius * shrinkFactor;
      } else {
        // Fire is still growing unchecked
        estimatedRadius = Math.min(elapsedMinutes * growthRate, 5000); // Cap at 5km
      }
      
      // Generate simple circular polygon (8 points)
      const polygon_coords = [];
      const centerLat = incident.lat || 0;
      const centerLon = incident.lon || 0;
      
      for (let i = 0; i < 8; i++) {
        const angle = (i / 8) * Math.PI * 2;
        const latOffset = (estimatedRadius / 111320) * Math.cos(angle);
        const lonOffset = (estimatedRadius / (111320 * Math.cos(centerLat * Math.PI / 180))) * Math.sin(angle);
        polygon_coords.push([centerLon + lonOffset, centerLat + latOffset]);
      }
      // Close the polygon
      polygon_coords.push(polygon_coords[0]);

      return {
        id: incident.id,
        name: incident.name,
        lat: incident.lat,
        lon: incident.lon,
        polygon_coords,
        estimated_radius: estimatedRadius,
        growth_rate: growthRate,
        risk_level: incident.risk,
        containment: incident.containment || 0,
        last_update: incident.last_update,
        description: incident.description
      };
    });

    // Format firestation data
    const formattedStations = (firestations || []).map((station: any) => ({
      id: station.id,
      name: station.name,
      lat: station.lat,
      lon: station.lon,
      city: station.city,
      county: station.county,
      active_route: null // Will be populated when routes are assigned
    }));

    return NextResponse.json({
      fires,
      firestations: formattedStations,
      timestamp: new Date().toISOString(),
      count: {
        active_fires: fires.length,
        firestations: formattedStations.length
      }
    });
  } catch (error) {
    console.error('Fire state API error:', error);
    return NextResponse.json(
      { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 }
    );
  }
}


```

### app/api/weather/route.ts

```typescript
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const lat = searchParams.get('lat');
  const lon = searchParams.get('lon');

  if (!lat || !lon) {
    console.error('Weather API: Missing parameters', { lat, lon });
    return NextResponse.json(
      { error: 'Missing latitude or longitude' },
      { status: 400 }
    );
  }

  // Validate coordinates
  const latitude = parseFloat(lat);
  const longitude = parseFloat(lon);
  
  if (isNaN(latitude) || isNaN(longitude) || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
    console.error('Weather API: Invalid coordinates', { lat, lon });
    return NextResponse.json(
      { error: 'Invalid latitude or longitude values' },
      { status: 400 }
    );
  }

  try {
    console.log(`Weather API: Fetching data for coordinates: ${lat}, ${lon}`);
    
    // Using Open-Meteo API (free, no API key required)
    // Fetching current weather with wind speed, direction, temperature, humidity, and visibility
    const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m,visibility&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=auto`;
    
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
    
    const weatherResponse = await fetch(weatherUrl, {
      signal: controller.signal,
      next: { revalidate: 300 }, // Cache for 5 minutes
      headers: {
        'Accept': 'application/json',
      },
    }).finally(() => clearTimeout(timeoutId));

    if (!weatherResponse.ok) {
      const errorText = await weatherResponse.text();
      console.error(`Weather API error: ${weatherResponse.status} ${weatherResponse.statusText}`, errorText);
      throw new Error(`Open-Meteo API returned ${weatherResponse.status}: ${errorText}`);
    }

    const weatherData = await weatherResponse.json();
    
    if (!weatherData.current) {
      console.error('Weather API: Missing current data in response', weatherData);
      throw new Error('Invalid weather data structure received');
    }
    
    const current = weatherData.current;

    // Fetch Air Quality data from Open-Meteo Air Quality API (non-blocking)
    let airQuality = null;
    try {
      const airQualityUrl = `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lon}&current=us_aqi,pm2_5,pm10&timezone=auto`;
      
      const aqController = new AbortController();
      const aqTimeoutId = setTimeout(() => aqController.abort(), 5000); // 5 second timeout for air quality
      
      const airQualityResponse = await fetch(airQualityUrl, {
        signal: aqController.signal,
        next: { revalidate: 300 }, // Cache for 5 minutes
        headers: {
          'Accept': 'application/json',
        },
      }).finally(() => clearTimeout(aqTimeoutId));

      if (airQualityResponse.ok) {
        const airQualityData = await airQualityResponse.json();
        if (airQualityData.current) {
          airQuality = {
            aqi: airQualityData.current.us_aqi || null,
            pm2_5: airQualityData.current.pm2_5 || null,
            pm10: airQualityData.current.pm10 || null,
          };
        }
      } else {
        console.warn('Air Quality API failed (non-critical):', airQualityResponse.status);
      }
    } catch (airQualityError) {
      console.warn('Air Quality API error (non-critical):', airQualityError);
      // Continue without air quality data
    }

    // Extract relevant weather information
    const completeWeatherData = {
      wind: {
        speed: Math.round(current.wind_speed_10m || 0), // mph
        deg: current.wind_direction_10m || 0, // degrees
        direction: getWindDirection(current.wind_direction_10m || 0),
      },
      temp: Math.round(current.temperature_2m || 0), // Fahrenheit
      humidity: current.relative_humidity_2m || 0, // percentage
      visibility: current.visibility ? Math.round(current.visibility / 1609.34) : null, // Convert meters to miles
      airQuality: airQuality,
      description: getWeatherDescription(current.weather_code || 0),
      location: 'Current Location',
    };

    console.log('Weather API: Successfully fetched data', { temp: completeWeatherData.temp, humidity: completeWeatherData.humidity });
    return NextResponse.json(completeWeatherData);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    
    // Check if it's an abort error (timeout)
    if (error instanceof Error && error.name === 'AbortError') {
      console.error('Weather API timeout:', errorMessage);
      return NextResponse.json(
        { 
          error: 'Weather service timeout',
          details: 'The weather service took too long to respond. Please try again.',
        },
        { status: 504 }
      );
    }
    
    console.error('Error fetching weather data:', errorMessage, error);
    return NextResponse.json(
      { 
        error: 'Failed to fetch weather data',
        details: errorMessage,
      },
      { status: 500 }
    );
  }
}

// Helper function to convert wind degrees to cardinal direction
function getWindDirection(degrees: number): string {
  const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
  const index = Math.round(((degrees % 360) / 22.5));
  return directions[index % 16];
}

// Helper function to convert Open-Meteo weather codes to descriptions
function getWeatherDescription(code: number): string {
  const weatherCodes: { [key: number]: string } = {
    0: 'Clear sky',
    1: 'Mainly clear',
    2: 'Partly cloudy',
    3: 'Overcast',
    45: 'Foggy',
    48: 'Depositing rime fog',
    51: 'Light drizzle',
    53: 'Moderate drizzle',
    55: 'Dense drizzl
[truncated — 479 more characters]
```

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