# Project export: EmberWatch

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: AI-powered incident commander assistant that turns real-time weather, fire spread, geospatial, and resource data into clear insights—helping firefighters act fast in the first critical minutes.
- Devpost: https://devpost.com/software/wildfire-ai
- GitHub: https://github.com/ojas-sanghi/treehacks-2026-wildfires
- Demo: https://treehacks-2026-wildfires.vercel.app/
- Video: https://www.youtube.com/embed/aWIkSYSlMqw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Ojas Sanghi (15 commits), carissaxchen (8 commits), Cursor (7 commits)

## Devpost submission (written by the team)

### Inspiration

The first minutes of a fire determine whether it is contained or becomes a major disaster. Incident commanders must make rapid decisions about where to deploy resources and issue evacuation warnings. Although there is more data than ever to assist with this decision-making, there is presently no centralized platform that combines predictive modeling of fire spread, weather and topographical data, and historical maps of previous fires and constructed firebreaks, making it difficult for incident commanders to take advantage of the technology’s benefits.

### What it does

Our project synthesizes relevant information for an incident commander during the first minutes of a fire so they can seamlessly use mapping and modeling tools to their full extent. We provide concise, up-to-date alerts about the spread of the fire and visualizations that the user can use to understand and validate the natural language alerts.

### How we built it

We gathered data from numerous datasets related to factors relevant to incident commander decision-making in the early stages of a fire. The data included: (1) topography, (2) population density, (3) real-time wind data, (4) historical fire data, (5) firebreak locations. We also retrieved a model of wildfire spread to predict the fire’s trajectory so necessary evacuation notifications can be sent out before the fire spreads closer to those living in nearby population centers. We generated maps of all of these factors, and then leveraged a VLM to synthesize the maps and provide useful recommendations to the incident commander as well as relevant visualizations. To make the process as seamless as possible for the user, we chose a progressive web app interface so the information can be accessible from an cell phone.

### Challenges we ran into

Our first challenge was settling on a project. On Friday night we brainstormed ideas, which ranged from campus bike rental services to identification of high-risk power lines in the event of a disaster. It was only after perusing the sustainability track challenges that we settled on our idea, which was solidified on Saturday after speaking to Jake Hess, the challenge mentor, who provided us with more information about the problem this project would help firefighters overcome. From here, some challenges we ran into were finding viable datasets for all of our dimensions of interest. Either datasets were too coarse or too granular, or the APIs were simply frustrating to deal with. We struggled in particular with our model of wildfire spread; simply finding a wildfire simulator that was accessible and extendable was extremely difficult, primarily due to dataset incompatibility. And because we gathered data from different sources, another challenge was merging the data formats into a common mapping space that we could use for inference and visualization. Our next biggest hurdle was conceptualizing how to create meaningful natural language analysis and advice for firefighters without risking providing false information. To overcome this, we provided an AI model with raw numerical data in addition to qualitative charts of each of our dimensions in order to gather both quantitative and qualitative descriptions.

### Accomplishments we're proud of

This being our first hackathon, we ran into many challenges. However, in these challenges we also were able to grow our technical skills. From dealing with geospatial data embedded into an interactive webapp to aggregating all of our datasets to predict wildfire spread, our data analysis skills are something that we are all proud to say we are stronger at. What was at first an ambitious idea—to build a model predicting wildfire spread—became easier as we broke the problem down into smaller, digestible parts. We also navigated together through using Git for a highly collaborative project, and were able to successfully delegate tasks from UI/UX, implementation of a wildfire prediction API, and the processing and visualization of our datasets all at once. We are proud of our collective commitment to providing mutual assistance to each other and always making sure no one was stuck on a task for too long.

### What we learned

We learned that a product is only as strong the need it addresses, and it is vital to talk to our target users to better understand the pain points more precisely. Our pivotal moment was when we talked to Jake Hess, who served at CAL FIRE as the Assistant Region Chief of Northern California after serving for 29 years. He clearly explained the gap between the large amounts of data that exist in disparate locations and the time constraints firefighters face during a real-world fire situation, and he outlined what kinds of alerts would be most helpful to firefighters in the moment.

### What's next

We are so excited to announce we will be signing with a production company to create our first album called Mr. AI. You can get a sneak peek of our first single (and title song!) here: https://suno.com/s/2qN4DSnG3HOiFQGk

## README (from the GitHub repository)

# TreeHacks 2026 - Wildfire Analysis & Visualization Platform

A comprehensive wildfire analysis platform combining real-time data visualization, population risk assessment, and an AI-powered alert system for wildfire incident commanders.

## 🔥 Project Overview

This project consists of two main components:

1. **Data Analysis & Visualization** - Python-based geospatial analysis generating wildfire risk visualizations
2. **Alert Hub Web App** - Next.js application with swipe-based task management for incident response

### Run the Wildfire Sim Platform page

The simulator page now lives inside this app at:

- `http://localhost:3000/wildfire-sim-platform`
- `http://localhost:3000/simulator` (legacy route)

Start both services from `treehacks-2026-wildfires`:

```bash
# Terminal 1: simulation API
./simulation/run_api.sh

# Terminal 2: Next.js app
npm run dev
```

If you want to customize backend settings, copy:

```bash
cp simulation/backend/.env.example simulation/backend/.env
```

## 📁 Project Structure

```
treehacks-2026-wildfires/
├── app/                          # Next.js web application
│   ├── components/              # React components
│   ├── globals.css              # Global styles with notification animations
│   ├── layout.tsx               # Root layout
│   └── page.tsx                 # Main page with Alert Hub & swipe functionality
├── data/
│   ├── input/                   # Input data files
│   │   ├── tl_2020_06_bg.*     # Census shapefiles (California block groups)
│   │   ├── USGS_13_n38w123.tif # Digital Elevation Model (DEM)
│   │   ├── 2025_Gaz_place_national.txt  # US Gazetteer (cities)
│   │   ├── fire24_1.gdb.zip    # Historic fire perimeters (optional)
│   │   └── CALFIRE_FuelReductionProjects.gdb.zip  # Fuel reduction zones (optional)
│   └── output/                  # Generated visualizations and data
│       ├── 1_topography.png
│       ├── 2_population_density.png
│       ├── 3_historic_fires.png (if data available)
│       ├── 4_fuel_reduction_zones.png (if data available)
│       ├── 5_wind_vectors.png
│       └── aggregated_wildfire_data.csv
├── docs/                        # Documentation files
│   ├── APP.md                   # Web app documentation
│   ├── DATA_SETUP.md           # Data setup guide
│   ├── INSTALL.md              # Installation instructions
│   ├── NOTIFICATION_SYSTEM.md  # Swipe notification system guide
│   ├── PROJECT_SUMMARY.md      # Project overview
│   └── SETUP_GUIDE.md          # Component setup
├── scripts/
│   └── generate_visualizations.py  # Main data processing script
├── public/                      # Static assets
├── package.json                 # Node.js dependencies
└── README.md                    # This file
```

## 🚀 Quick Start

### Prerequisites

- **Node.js 18+** (for web app)
- **Python 3.12+** with conda/miniconda (for data analysis)
- **Census API Key** ([Get one here](https://api.census.gov/data/key_signup.html))

### 1. Web Application Setup

```bash
# Install dependencies
npm install --legacy-peer-deps

# Start development server
npm run dev
```

Visit http://localhost:3000 to see the Alert Hub with swipe-based notification management.

**Live alerts:** The Alert Hub can generate alerts from `data/output/` (CSV + optional images) using the Gemini API. Copy `.env.local.example` to `.env.local` and set `GEMINI_API_KEY` (get a key from [Google AI Studio](https://aistudio.google.com/apikey)). Without it, the app shows fallback alerts.

### 2. Data Analysis Setup

```bash
# Install Python dependencies (using conda)
conda install -y pandas geopandas numpy matplotlib rasterio pyogrio shapely requests -c conda-forge

# Run visualization script
python scripts/generate_visualizations.py
```

**Outputs:**

- 3-5 PNG visualizations in `data/output/`
- 1 CSV dataframe with aggregated features

## 📊 Features

### Web Application

- **Swipe-Based Task Management**
  - Swipe ← left: Acknowledge & snooze (5 min)
  - Swipe → right: Mark in progress
  - Swipe →→ right again: Complete & resolve
- **Live Alert Hub** – AI-generated alerts from `data/output` (Gemini) or fallback; swipe to acknowledge/complete
- **Live ArcGIS Map Integration**
- **Persistent State** (localStorage)

### Data Analysis

- **Topography Visualization** - Elevation heatmap with hillshade
- **Population Density** - Logarithmic scale human footprint
- **Historic Fires** - Fire perimeter overlays (optional)
- **Fuel Reduction Zones** - CAL FIRE treatment areas (optional)
- **Wind Vectors** - Real-time wind speed/direction from Open-Meteo API
- **Aggregated CSV** - Gridded dataframe (50×50) with all features

## 🗂️ Data Sources

| Data Type          | Source                | Status          | Size      |
| ------------------ | --------------------- | --------------- | --------- |
| Census Population  | US Census API         | ✅ Auto-fetched | ~2.5MB    |
| Block Group Shapes | TIGER/Line Shapefiles | ✅ Included     | ~84MB     |
| Elevation (DEM)    | USGS National Map     | ✅ Included     | ~223MB    |
| US Cities          | Census Gazetteer      | ✅ Included     | ~3.3MB    |
| Historic Fires     | CAL FIRE GIS          | ⚠️ Optional     | TBD       |
| Fuel Reduction     | CAL FIRE GIS          | ⚠️ Optional     | TBD       |
| Wind Data          | Open-Meteo API        | ✅ Auto-fetched | Real-time |

### Adding Optional Data

To enable historic fire and fuel reduction visualizations:

1. Download from [California Open Data Portal](https://gis.data.ca.gov/)
   - Search for "fire perimeters" → `fire24_1.gdb.zip`
   - Search for "fuel reduction" → `CALFIRE_FuelReductionProjects.gdb.zip`
2. Place `.gdb.zip` files in `data/input/`
3. Re-run: `python scripts/generate_visualizations.py`

## ⚙️ Configuration

### Zoom Region (scripts/generate_visualizations.py)

```python
ZOOM_CONFIG = {
    'min_lat': 37.20,  # Adjust for your region
    'max_lat': 37.40,
    'min_lon': -122.40,
    'max_lon': -122.20
}
```

### Census API Key

Replace in `scripts/generate_visualizations.py`:

```python
API_KEY = "your_census_api_key_here"
```

## 🛠️ Development

### Web App Commands

```bash
npm run dev      # Start dev server (port 3000)
npm run build    # Build for production
npm run start    # Start production server
```

### Data Analysis

```bash
python scripts/generate_visualizations.py  # Generate all visualizations
```

**Runtime:** ~1-2 minutes (Census API + processing)

## 📚 Documentation

Quick access to all documentation:

- **[README.md](README.md)** ← You are here - Main project documentation
- **[QUICK_REFERENCE.md](docs/QUICK_REFERENCE.md)** - Quick command reference & file locations
- **[FILE_ORGANIZATION.md](docs/FILE_ORGANIZATION.md)** - Before/after file structure
- **[INSTALL.md](docs/INSTALL.md)** - Detailed installation guide
- **[DATA_SETUP.md](docs/DATA_SETUP.md)** - Data acquisition & setup
- **[NOTIFICATION_SYSTEM.md](docs/NOTIFICATION_SYSTEM.md)** - Swipe gesture documentation
- **[APP.md](docs/APP.md)** - Web application features
- **[PROJECT_SUMMARY.md](docs/PROJECT_SUMMARY.md)** - Project overview

## 🎯 Use Cases

1. **Incident Command** - Real-time alert prioritization for fire crews
2. **Risk Assessment** - Population density + terrain + fire history analysis
3. **Resource Planning** - Identify high-risk areas needing fuel reduction
4. **Decision Support** - Wind-informed evacuation routing
5. **Historical Analysis** - Pattern matching with past fire behavior

## 🤝 Contributing

Built for TreeHacks 2026. Contributions welcome!

## 📖 References

Filippi, J.-B., Baggio, R., Paugam, R., Bosseur, F., Leblanc, A., & Alonso-Pinar, A. (2025). ForeFire: A Modular, Scriptable C++ Simulation Engine and Library for Wildland-Fire Spread. *Journal of Open Source Software*, 10(116), 8680. https://doi.org/10.21105/joss.08680

## 🙏 Acknowledgments

Special thanks to **Jake Hess from CAL FIRE** for providing valuable guidance and data access.

## 📄 License

MIT License

## 🔗 Links

- **Census API:** https

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 53 recognized source files, 317 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (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
- Google Gemini (technology) — claimed on Devpost, not found in the code
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (71 of 71)

```
.env.local.example
.gitignore
.python-version
app/_components/body-background.tsx
app/_components/conditional-header.tsx
app/api/alerts/route.ts
app/api/decision-support/route.ts
app/api/output-map/route.ts
app/api/simulation/route.ts
app/api/simulator/[...path]/route.ts
app/api/wildfire-sim-platform/[...path]/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
app/simulator/page.tsx
app/wildfire-sim-platform/_components/fire-map.tsx
app/wildfire-sim-platform/_components/simulation-dashboard.tsx
app/wildfire-sim-platform/_lib/api.ts
app/wildfire-sim-platform/_lib/types.ts
app/wildfire-sim-platform/layout.tsx
app/wildfire-sim-platform/page.tsx
app/wildfire-sim-platform/styles.css
data/input/tl_2020_06_bg.cpg
data/input/tl_2020_06_bg.prj
data/input/tl_2020_06_bg.shp.ea.iso.xml
data/input/tl_2020_06_bg.shp.iso.xml
data/output/aggregated_wildfire_data.csv
docs/APP.md
docs/DATA_SETUP.md
docs/FILE_ORGANIZATION.md
docs/INSTALL.md
docs/NOTIFICATION_SYSTEM.md
docs/PROJECT_SUMMARY.md
docs/QUICK_REFERENCE.md
docs/SETUP_GUIDE.md
lib/fire-simulation.ts
lib/gemini.ts
lib/notification-sound.ts
next.config.js
package.json
postcss.config.js
pyproject.toml
README.md
scripts/generate_visualizations.py
simulation/backend/.env.example
simulation/backend/app/__init__.py
simulation/backend/app/config.py
simulation/backend/app/data/layers/fuel_reduction.geojson
simulation/backend/app/data/layers/historic_fires.geojson
simulation/backend/app/data/layers/layers.manifest.json
simulation/backend/app/data/layers/population_density.geojson
simulation/backend/app/data/layers/topography_contours.geojson
simulation/backend/app/data/layers/wind_vectors.geojson
simulation/backend/app/engines/__init__.py
simulation/backend/app/engines/base.py
simulation/backend/app/engines/forefire_cli.py
simulation/backend/app/engines/forefire_docker.py
simulation/backend/app/engines/prior_adaptation.py
simulation/backend/app/main.py
simulation/backend/app/models.py
simulation/backend/app/runs/.gitkeep
simulation/backend/app/services/__init__.py
simulation/backend/app/services/layer_service.py
simulation/backend/app/services/simulation_service.py
simulation/backend/README.md
simulation/backend/scripts/convert_geodataframe.py
simulation/README.md
simulation/run_api.sh
tailwind.config.js
tsconfig.json
uv.lock
```

### Dependencies

- package.json: @google/genai@^1.41.0, @types/leaflet@^1.9.14, @types/node@^22.10.2, @types/react@^19.0.1, @types/react-dom@^19.0.2, autoprefixer@^10.4.20, clsx@^2.1.1, date-fns@^4.1.0, eslint@^9.16.0, eslint-config-next@^15.1.0, framer-motion@^11.15.0, leaflet@^1.9.4, lucide-react@^0.469.0, maplibre-gl@^4.7.1, next@^15.1.0, postcss@^8.4.49, react@^19.0.0, react-dom@^19.0.0, react-leaflet@^4.2.1, react-map-gl@^7.1.9, recharts@^2.13.3, tailwindcss@^3.4.17, typescript@^5.7.2
- pyproject.toml: fastapi@>=0.115.0, geojson-pydantic@>=0.6.0, geopandas@>=1.0.1, netcdf4@>=1.6.0, pydantic@>=2.9.0, pyproj@>=3.6.1, python-dotenv@>=1.0.1, python-multipart@>=0.0.12, rasterio@>=1.4.4, requests@>=2.32.5, shapely@>=2.0.6, uvicorn[standard]@>=0.32.0, xarray@>=2024.0

### Recent commits (newest first)

- s
- Merge branch 'main' of https://github.com/ojas-sanghi/treehacks-2026-wildfires
- finished sytling
- lc 5
- lc 4
- location 3
- new location test
- fix data file location
- credits
- move wildfire sim into  main page
- finish adding stuff
- Merge branch 'main' of https://github.com/ojas-sanghi/treehacks-2026-wildfires
- added start simulation + logo
- fix issues with parameters. amke sure it all works
- new orange background
- Merge branch 'main' of https://github.com/ojas-sanghi/treehacks-2026-wildfires
- styling
- integrate many layers into view
- Merge branch 'main' of https://github.com/ojas-sanghi/treehacks-2026-wildfires
- Merge branch 'main' of https://github.com/ojas-sanghi/treehacks-2026-wildfires

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

### docs/INSTALL.md

```markdown
# Fire AI - Installation Instructions

## Quick Install

1. **Navigate to the project directory**:
   ```bash
   cd ~/Desktop/"Fire AI"
   ```

2. **Install dependencies**:
   ```bash
   npm install
   ```

3. **Start the development server**:
   ```bash
   npm run dev
   ```

4. **Open in browser**:
   Navigate to `http://localhost:3000`

## What You Get

✅ Next.js 15 + React 19 + TypeScript
✅ Tailwind CSS with custom fire-themed design
✅ Responsive dashboard layout
✅ Professional UI with animations
✅ Ready for fire data integration
✅ API routes structure
✅ Component architecture

## Project Status

🟢 **Ready to run** - Basic application is functional
🟡 **Needs API keys** - For real fire and weather data
🟡 **Needs components** - Full components in SETUP_GUIDE.md

## Next Steps

1. **Add Components** - See SETUP_GUIDE.md for full component code
2. **Get API Keys** - NASA FIRMS, OpenWeather, Mapbox (optional)
3. **Customize** - Modify colors, features, data sources
4. **Deploy** - Use Vercel, AWS, or your preferred platform

## File Structure

```
Fire AI/
├── app/
│   ├── layout.tsx       ✅ Created
│   ├── page.tsx         ✅ Created  
│   ├── globals.css      ✅ Created
│   ├── components/      📁 Ready for your components
│   ├── api/             📁 Ready for API routes
│   └── lib/             📁 Ready for utilities
├── package.json         ✅ Created
├── next.config.js       ✅ Created
├── tsconfig.json        ✅ Created
├── tailwind.config.js   ✅ Created
├── .env.local.example   ✅ Created
├── .gitignore           ✅ Created
├── README.md            ✅ Created
├── SETUP_GUIDE.md       ✅ Created
└── INSTALL.md           ✅ This file
```

## Troubleshooting

**Port 3000 in use?**
```bash
npm run dev -- -p 3001
```

**Installation errors?**
```bash
rm -rf node_modules package-lock.json
npm install
```

**Node version?**
Requires Node.js 18+. Check with: `node -v`

---

For questions or issues, refer to README.md or SETUP_GUIDE.md

```

### docs/APP.md

```markdown
# Making Fire AI an App

Your project is set up as a **Progressive Web App (PWA)**. Users can install it on their phone or desktop and open it like a native app.

---

## Install on your device

### iPhone / iPad (Safari)

1. Open your site in **Safari** (e.g. `https://your-domain.com` or `http://localhost:3000` when testing).
2. Tap the **Share** button.
3. Tap **Add to Home Screen**.
4. Name it “Fire AI” and tap **Add**.

The icon appears on your home screen and opens in full-screen (no browser UI).

### Android (Chrome)

1. Open your site in **Chrome**.
2. Tap the **menu** (⋮) → **Install app** or **Add to Home Screen** (or you may see an install banner).
3. Confirm the name and tap **Install**.

### Desktop (Chrome / Edge)

1. Open your site in Chrome or Edge.
2. Look for an **Install** icon in the address bar, or use the menu → **Install Fire AI…**.
3. Confirm to add it to your apps/desktop.

---

## What’s included

- **Web app manifest** (`public/manifest.webmanifest`) – name, theme color, icons, standalone display.
- **App icon** – `public/icons/icon.svg` (optional: add `icon-192.png` and `icon-512.png` for best support; see `public/icons/README.md`).
- **Layout metadata** – theme color, Apple web app flags, and viewport so it behaves like an app when installed.

---

## Deploy so others can install

The install prompt only appears when the app is served over **HTTPS** (and on localhost for testing).

1. Deploy to a host that provides HTTPS, for example:
   - [Vercel](https://vercel.com) (works well with Next.js)
   - Netlify, Railway, or your own server with SSL
2. Share the URL; users on phones and desktops can then use the steps above to install.

---

## Optional: native app (App Store / Play Store)

To ship Fire AI as a native app in the iOS App Store or Google Play Store, you can wrap the same Next.js app with:

- **[Capacitor](https://capacitorjs.com)** – build a native shell that loads your deployed PWA or a local build. One codebase for web + iOS + Android.
- **[PWA Builder](https://www.pwabuilder.com)** – can package your PWA for the Microsoft Store and help with store submission.

The PWA setup you have now is enough for “install on device” and app-like behavior without going through the stores.

```

### pyproject.toml

```
[project]
name = "treehacks-2026-wildfires"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115.0",
    "geojson-pydantic>=0.6.0",
    "netcdf4>=1.6.0",
    "python-multipart>=0.0.12",
    "uvicorn[standard]>=0.32.0",
    "xarray>=2024.0",
    "pydantic>=2.9.0",
    "geopandas>=1.0.1",
    "shapely>=2.0.6",
    "pyproj>=3.6.1",
    "python-dotenv>=1.0.1",
    "rasterio>=1.4.4",
    "requests>=2.32.5",
]

```

### package.json

```
{
  "name": "fire-ai",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@google/genai": "^1.41.0",
    "clsx": "^2.1.1",
    "date-fns": "^4.1.0",
    "framer-motion": "^11.15.0",
    "leaflet": "^1.9.4",
    "lucide-react": "^0.469.0",
    "next": "^15.1.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-leaflet": "^4.2.1",
    "recharts": "^2.13.3",
    "maplibre-gl": "^4.7.1",
    "react-map-gl": "^7.1.9"
  },
  "devDependencies": {
    "@types/leaflet": "^1.9.14",
    "@types/node": "^22.10.2",
    "@types/react": "^19.0.1",
    "@types/react-dom": "^19.0.2",
    "autoprefixer": "^10.4.20",
    "typescript": "^5.7.2",
    "eslint": "^9.16.0",
    "eslint-config-next": "^15.1.0",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.17"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata, Viewport } from 'next'
import { Inter, Outfit } from 'next/font/google'
import './globals.css'
import './wildfire-sim-platform/styles.css'
import { BodyBackground } from './_components/body-background'
import { ConditionalHeader } from './_components/conditional-header'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-sans',
  display: 'swap',
  weight: ['400', '500', '600', '700'],
})

const outfit = Outfit({
  subsets: ['latin'],
  variable: '--font-brand',
  display: 'swap',
  weight: ['400', '500', '600', '700'],
})

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  viewportFit: 'cover',
  themeColor: '#1e293b',
}

export const metadata: Metadata = {
  title: 'Ember Watch — Wildfire Decision Support',
  description: 'Supporting incident commanders in the first minutes of response. Ojas Sanghi, Carissa Chen, Eshaan Kothari, David Tomz.',
  manifest: '/manifest.webmanifest',
  appleWebApp: {
    capable: true,
    statusBarStyle: 'black-translucent',
    title: 'Ember Watch',
  },
  icons: {
    icon: [
      { url: '/logo.png', type: 'image/png', sizes: '32x32' },
      { url: '/logo.png', type: 'image/png', sizes: '192x192' },
      { url: '/logo.png', type: 'image/png', sizes: '512x512' },
    ],
  },
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.variable} ${outfit.variable}`} suppressHydrationWarning>
      <body className="font-sans min-h-screen text-slate-700" suppressHydrationWarning>
        <BodyBackground />
        <ConditionalHeader />
        {children}
      </body>
    </html>
  )
}

```

### app/wildfire-sim-platform/page.tsx

```typescript
import { SimulationDashboard } from './_components/simulation-dashboard'

export default function WildfireSimPlatformPage() {
  return <SimulationDashboard />
}

```

### app/wildfire-sim-platform/layout.tsx

```typescript
import type { Metadata } from 'next'
import { IBM_Plex_Mono, Sora } from 'next/font/google'

const sora = Sora({
  subsets: ['latin'],
  variable: '--font-display',
  weight: ['400', '500', '600', '700'],
  display: 'swap',
})

const ibmPlexMono = IBM_Plex_Mono({
  subsets: ['latin'],
  variable: '--font-mono',
  weight: ['400', '500'],
  display: 'swap',
})

export const metadata: Metadata = {
  title: 'Wildfire Simulation Lab',
  description: 'ForeFire-powered wildfire simulation with timeline playback and map overlays',
}

export default function WildfireSimLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <div className={`wildfire-sim-platform-root ${sora.variable} ${ibmPlexMono.variable}`}>
      {children}
    </div>
  )
}

```

### app/page.tsx

```typescript
"use client";

import { playNotificationSound } from "@/lib/notification-sound";
import { SimulationDashboard } from "./wildfire-sim-platform/_components/simulation-dashboard";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";

type NotificationStatus =
  | "default"
  | "acknowledged"
  | "inProgress"
  | "resolved";

interface Notification {
  id: string;
  source: string;
  time: string;
  message: string;
  more?: number;
  older?: boolean;
  status: NotificationStatus;
  timestamp: number;
  snoozeUntil?: number;
}

interface SimulationEvent {
  simulatedHours: number;
  source: string;
  message: string;
}

const SIM_SECONDS_PER_HOUR = 6;
const SNOOZE_MS = 5 * 60 * 1000;

function isNotificationStatus(value: unknown): value is NotificationStatus {
  return (
    value === "default" ||
    value === "acknowledged" ||
    value === "inProgress" ||
    value === "resolved"
  );
}

function parseStoredNotifications(raw: string | null): Notification[] {
  if (!raw) return [];

  try {
    const parsed = JSON.parse(raw) as unknown;
    if (!Array.isArray(parsed)) return [];

    const safe: Notification[] = [];
    for (const item of parsed) {
      if (!item || typeof item !== "object") continue;

      const candidate = item as Partial<Notification>;
      if (
        typeof candidate.id !== "string" ||
        typeof candidate.source !== "string" ||
        typeof candidate.time !== "string" ||
        typeof candidate.message !== "string" ||
        typeof candidate.timestamp !== "number" ||
        !isNotificationStatus(candidate.status)
      ) {
        continue;
      }

      safe.push({
        id: candidate.id,
        source: candidate.source,
        time: candidate.time,
        message: candidate.message,
        timestamp: candidate.timestamp,
        status: candidate.status,
        more:
          typeof candidate.more === "number" ? Math.floor(candidate.more) : undefined,
        older: Boolean(candidate.older),
        snoozeUntil:
          typeof candidate.snoozeUntil === "number"
            ? candidate.snoozeUntil
            : undefined,
      });
    }

    return safe;
  } catch {
    return [];
  }
}

function withOlderFlags(items: Notification[]): Notification[] {
  return items.map((item, index) => ({
    ...item,
    older: index >= 4,
  }));
}

export default function Home() {
  const [notifications, setNotifications] = useState<Notification[]>(() => {
    if (typeof window === "undefined") return [];
    return parseStoredNotifications(window.localStorage.getItem("notifications"));
  });
  const [simulationEvents, setSimulationEvents] = useState<SimulationEvent[]>([]);
  const [currentSimHour, setCurrentSimHour] = useState(0);
  const [simulationRunning, setSimulationRunning] = useState(false);
  const [simulationLoaded, setSimulationLoaded] = useState(false);
  const [simulationError, setSimulationError] = useState<string | null>(null);
  const [bubbleBump, setBubbleBump] = useState(false);
  const [showStartPage, setShowStartPage] = useState(true);
  const [mapAutoRunToken, setMapAutoRunToken] = useState(0);

  const lastEmittedHourRef = useRef(-1);
  const prevNotificationCountRef = useRef(notifications.length);

  const startFireSimulation = () => {
    setShowStartPage(false);
    setMapAutoRunToken((prev) => prev + 1);
    sessionStorage.setItem("fire-ai-started", "true");
    setNotifications([]);
    setCurrentSimHour(0);
    setSimulationEvents([]);
    setSimulationRunning(false);
    setSimulationLoaded(false);
    setSimulationError(null);
    lastEmittedHourRef.current = -1;
    prevNotificationCountRef.current = 0;
  };

  useEffect(() => {
    if (showStartPage) return;

    let cancelled = false;
    setSimulationError(null);
    setSimulationLoaded(false);

    fetch("/api/simulation")
      .then(async (res) => {
        if (!res.ok) {
          let message = `Simulation request failed (${res.status})`;
          try {
            const body = (await res.json()) as { error?: string; details?: string };
            if (body.details) message = body.details;
            else if (body.error) message = body.error;
          } catch {
            // keep default message
          }
          throw new Error(message);
        }

        return res.json() as Promise<{ events?: SimulationEvent[] }>;
      })
      .then((data) => {
        if (cancelled) return;

        const events = data.events;
        if (!Array.isArray(events) || events.length === 0) {
          throw new Error("Simulation returned no timeline events.");
        }

        setSimulationEvents(events);
        setSimulationRunning(true);
        setCurrentSimHour(0);
        lastEmittedHourRef.current = -1;
      })
      .catch((err) => {
        if (cancelled) return;
        const message =
          err instanceof Error ? err.message : "Could not load simulation timeline.";
        setSimulationError(message);
        setSimulationEvents([]);
        setSimulationRunning(false);
      })
      .finally(() => {
        if (!cancelled) setSimulationLoaded(true);
      });

    return () => {
      cancelled = true;
    };
  }, [showStartPage]);

  useEffect(() => {
    if (showStartPage || !simulationRunning || simulationEvents.length === 0) return;

    const totalHours = Math.max(...simulationEvents.map((event) => event.simulatedHours), 0);

    const interval = setInterval(() => {
      setCurrentSimHour((hour) => {
        const next = hour + 1;
        if (next > totalHours) {
          setSimulationRunning(false);
          return hour;
        }
        return next;
      });
    }, SIM_SECONDS_PER_HOUR * 1000);

    return () => clearInterval(interval);
  }, [showStartPage, simulationRunning, simulationEvents]);

  useEffect(() => {
    if (showStartPage || simulationEvents.length === 0) return;
    if (currentSimHour <= lastEmittedHourRef.current) return;

    const emittedHour = currentSimHour;
  
[truncated — 22797 more characters]
```

### app/simulator/page.tsx

```typescript
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'
import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { Play, Pause, ChevronLeft, ChevronRight, Layers, Flame, Loader2 } from 'lucide-react'
import { clsx } from 'clsx'

// Use same-origin proxy by default so the Python API is reached via Next.js (no CORS). Set NEXT_PUBLIC_SIMULATION_API to call the API directly.
const API_BASE =
  typeof process.env.NEXT_PUBLIC_SIMULATION_API === 'string' &&
  process.env.NEXT_PUBLIC_SIMULATION_API
    ? process.env.NEXT_PUBLIC_SIMULATION_API
    : ''

type SimState = 'idle' | 'running' | 'done' | 'error'

export default function SimulatorPage() {
  const mapContainerRef = useRef<HTMLDivElement>(null)
  const mapRef = useRef<maplibregl.Map | null>(null)

  const [lat, setLat] = useState(42.0)
  const [lon, setLon] = useState(9.1)
  const [duration, setDuration] = useState(7200)
  const [windU, setWindU] = useState<string>('')
  const [windV, setWindV] = useState<string>('')

  const [simState, setSimState] = useState<SimState>('idle')
  const [runId, setRunId] = useState<string | null>(null)
  const [timesteps, setTimesteps] = useState<number[]>([])
  const [perimeters, setPerimeters] = useState<Record<string, unknown>>({})
  const [currentIndex, setCurrentIndex] = useState(0)
  const [playing, setPlaying] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)
  const [metadata, setMetadata] = useState<Record<string, unknown>>({})

  const [layers, setLayers] = useState<Record<string, boolean>>({
    population: false,
    topography: false,
    historic_fires: false,
    fuel_reduction: false,
  })
  const layerSourcesRef = useRef<Record<string, string>>({})

  const playIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  const initMap = useCallback(() => {
    if (!mapContainerRef.current || mapRef.current) return
    const map = new maplibregl.Map({
      container: mapContainerRef.current,
      style: {
        version: 8,
        sources: {
          osm: {
            type: 'raster',
            tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
            tileSize: 256,
            attribution: '© OpenStreetMap',
          },
        },
        layers: [
          {
            id: 'osm',
            type: 'raster',
            source: 'osm',
            minzoom: 0,
            maxzoom: 19,
          },
        ],
      },
      center: [lon, lat],
      zoom: 10,
    })
    map.addControl(new maplibregl.NavigationControl(), 'top-right')
    mapRef.current = map
    return () => {
      map.remove()
      mapRef.current = null
    }
  }, [lat, lon])

  useEffect(() => {
    initMap()
    return () => {
      if (playIntervalRef.current) clearInterval(playIntervalRef.current)
    }
  }, [])

  const simulateUrl = API_BASE ? `${API_BASE}/api/simulate` : '/api/simulator/simulate'

  const runSimulation = async () => {
    setSimState('running')
    setErrorMessage(null)
    try {
      const res = await fetch(simulateUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          lat,
          lon,
          duration_seconds: duration,
          output_interval_seconds: 300,
          wind_u: windU === '' ? null : parseFloat(windU),
          wind_v: windV === '' ? null : parseFloat(windV),
        }),
      })
      const data = await res.json().catch(() => ({ detail: 'Invalid response from API' }))
      if (!res.ok) {
        throw new Error(
          typeof data.detail === 'string' ? data.detail : 'Simulation failed'
        )
      }
      setRunId(data.run_id)
      setTimesteps(data.timesteps_seconds || [])
      setPerimeters(data.perimeters || {})
      setMetadata(data.metadata || {})
      setCurrentIndex(0)
      setSimState(data.success ? 'done' : 'error')
      if (!data.success) setErrorMessage(data.message || 'Simulation failed')
      if (data.success && mapRef.current) {
        mapRef.current.flyTo({ center: [lon, lat], zoom: 11 })
      }
    } catch (e) {
      setSimState('error')
      const msg = e instanceof Error ? e.message : String(e)
      const isNetworkError =
        msg.includes('fetch') ||
        msg.includes('NetworkError') ||
        msg.includes('not running') ||
        msg.includes('Failed to fetch') ||
        (e instanceof TypeError && msg.includes('fetch'))
      setErrorMessage(
        isNetworkError
          ? "Simulation API isn't running. Start it in a terminal: ./simulation/run_api.sh"
          : msg
      )
    }
  }

  const currentT = timesteps[currentIndex] ?? 0
  const currentGeoJSON = perimeters[`t_${currentT}`] as { features?: unknown[] } | undefined

  useEffect(() => {
    const map = mapRef.current
    if (!map) return
    if (map.getLayer('fire-perimeter')) {
      map.removeLayer('fire-perimeter')
      const src = map.getSource('fire-perimeter')
      if (src) map.removeSource('fire-perimeter')
    }
    if (currentGeoJSON && currentGeoJSON.features?.length) {
      map.addSource('fire-perimeter', {
        type: 'geojson',
        data: currentGeoJSON as GeoJSON.FeatureCollection,
      })
      map.addLayer({
        id: 'fire-perimeter',
        type: 'fill',
        source: 'fire-perimeter',
        paint: {
          'fill-color': '#ea580c',
          'fill-opacity': 0.45,
          'fill-outline-color': '#c2410c',
        },
      })
      map.addLayer({
        id: 'fire-perimeter-line',
        type: 'line',
        source: 'fire-perimeter',
        paint: {
          'line-color': '#c2410c',
          'line-width': 2,
        },
      })
    }
    return () => {
      if (map.getLayer('fire-perimeter')) map.removeLayer('fire-perimeter')
      if (map.getLayer('fire-perimeter-line')) map.removeLayer('fire-perimeter-line')
      if (map.getSource('fire-perimeter')) map.removeSource('fire-perimeter')
    }
  }, [currentGeoJSON])

  useEffect(() 
[truncated — 11967 more characters]
```

### app/api/output-map/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { readFileSync, existsSync } from "fs";
import path from "path";

const ALLOWED_NAMES = [
  "1_topography.png",
  "2_population_density.png",
  "3_historic_fires.png",
  "4_fuel_reduction_zones.png",
  "5_wind_vectors.png",
];

export async function GET(request: NextRequest) {
  const name = request.nextUrl.searchParams.get("name");
  if (!name || !ALLOWED_NAMES.includes(name)) {
    return NextResponse.json({ error: "Invalid or missing name" }, { status: 400 });
  }

  const outputDir = path.join(process.cwd(), "data", "output");
  const filePath = path.join(outputDir, name);

  if (!existsSync(filePath)) {
    return new NextResponse(null, { status: 404 });
  }

  try {
    const buffer = readFileSync(filePath);
    return new NextResponse(buffer, {
      headers: {
        "Content-Type": "image/png",
        "Cache-Control": "public, max-age=3600",
      },
    });
  } catch {
    return new NextResponse(null, { status: 500 });
  }
}

```

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