diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..047198f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,59 @@
+name: CI
+
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main, develop]
+
+jobs:
+ python:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libgl1
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install -e .
+
+ - name: Lint with flake8
+ run: |
+ flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics
+ flake8 src/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+
+ - name: Test with pytest
+ run: |
+ pytest tests/ -v --tb=short
+
+ frontend:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: frontend
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: "npm"
+ cache-dependency-path: frontend/package-lock.json
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Type check and build
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index 9f24f71..4ba3bec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,8 +40,8 @@ ENV/
!notebooks/*.ipynb
# Data
-data/
-datasets/
+/data/
+/datasets/
*.tif
*.tiff
*.h5
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bcba074..d29cd37 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,7 +31,33 @@ We are committed to providing a welcoming and inclusive environment. Please be r
#### First Time Contributors
-Look for issues labeled `good first issue` - these are specifically chosen for newcomers.
+Look for issues labeled `good first issue` — these are specifically chosen for newcomers.
+
+**Recommended first issues (ready to pick up):**
+
+| Issue | What You'll Learn | Time Estimate |
+|-------|-----------------|---------------|
+| [#9: Add frontend unit tests](https://github.com/Climate-Vision/ClimateVision/issues/9) | Vitest, React Testing Library, Vite | 2–4 hours |
+| [#13: Add Docker Compose](https://github.com/Climate-Vision/ClimateVision/issues/13) | Docker, multi-service orchestration | 3–6 hours |
+
+**How to claim an issue:**
+1. Read the issue description and acceptance criteria
+2. Comment "I'd like to work on this" — a maintainer will assign you
+3. Fork the repo and create a branch: `git checkout -b feature/issue-9-frontend-tests`
+4. Open a **draft PR** within 48 hours (even if incomplete) so we can give early feedback
+
+**Need help?** Tag `@Climate-Vision/maintainers` in the issue or open a [Discussion](https://github.com/Climate-Vision/ClimateVision/discussions).
+
+#### Intermediate Contributors
+
+Ready for something meatier? These issues close critical gaps in our production pipeline:
+
+| Issue | Area | Skills You'll Build |
+|-------|------|-------------------|
+| [#10: Alert delivery worker](https://github.com/Climate-Vision/ClimateVision/issues/10) | Backend | FastAPI BackgroundTasks, SMTP, webhooks |
+| [#11: WebSocket real-time updates](https://github.com/Climate-Vision/ClimateVision/issues/11) | Full-stack | FastAPI WebSockets, React hooks, graceful degradation |
+| [#12: ONNX Runtime inference](https://github.com/Climate-Vision/ClimateVision/issues/12) | MLOps | ONNX Runtime, PyTorch export, latency benchmarking |
+| [#14: Carbon analytics API](https://github.com/Climate-Vision/ClimateVision/issues/14) | Analytics | Feature flags, API schema design, geospatial math |
#### Development Process
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index e72eb07..5991563 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -190,6 +190,7 @@ export async function predictJson(payload: PredictJsonRequest): Promise<{
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
+
if (!res.ok) throw new Error('Prediction failed')
return res.json()
}
diff --git a/frontend/src/components/ui/ApiError.tsx b/frontend/src/components/ui/ApiError.tsx
new file mode 100644
index 0000000..d8328c3
--- /dev/null
+++ b/frontend/src/components/ui/ApiError.tsx
@@ -0,0 +1,25 @@
+// frontend/src/components/ui/ApiError.tsx
+import { AlertCircle, X } from 'lucide-react'
+import { useState } from 'react'
+
+type ApiErrorProps = {
+ message: string
+}
+
+export function ApiError({ message }: ApiErrorProps) {
+ const [visible, setVisible] = useState(true)
+
+ if (!visible) return null
+
+ return (
+
+
+
+
{message}
+
+
+
+ )
+}
diff --git a/frontend/src/pages/NewAnalysis.tsx b/frontend/src/pages/NewAnalysis.tsx
index e992b81..16de690 100644
--- a/frontend/src/pages/NewAnalysis.tsx
+++ b/frontend/src/pages/NewAnalysis.tsx
@@ -3,13 +3,14 @@ import { useNavigate } from 'react-router-dom'
import { Loader2 } from 'lucide-react'
import type { AnalysisType } from '../api'
import { predictJson } from '../api'
-import { MapBBoxPicker } from '../components/map/MapBBoxPicker'
+import { MapBBoxPicker } from '../components/Map/MapBBoxPicker'
import { AnalysisTypeSelector } from '../components/ui/AnalysisTypeSelector'
import { ResultsPanel } from '../components/results/ResultsPanel'
import { ErrorBoundary } from '../components/ui/ErrorBoundary'
import { useToast } from '../contexts/ToastContext'
import { useApp } from '../contexts/AppContext'
import type { Run } from '../api'
+import { ApiError } from '../components/ui/ApiError'
const PRESETS = [
{ label: 'Last 30d', days: 30 },
@@ -33,6 +34,7 @@ function SectionLabel({ step, label }: { step: number; label: string }) {
}
export default function NewAnalysis() {
+ const [error, setError] = useState(null)
const { showToast } = useToast()
const { googleMapsApiKey } = useApp()
const navigate = useNavigate()
@@ -61,7 +63,7 @@ export default function NewAnalysis() {
showToast('error', 'Start date must be before end date.')
return
}
-
+ setError(null)
setBusy(true)
setResultRun(null)
setResultPayload(null)
@@ -85,8 +87,10 @@ export default function NewAnalysis() {
label: 'View in history',
onClick: () => navigate('/runs'),
})
- } catch (e) {
- showToast('error', String(e))
+ } catch (e: any) {
+ const message = e?.response?.data?.detail || e?.response?.data?.message || e?.message || 'Prediction failed'
+ setError(message)
+ showToast('error', message)
} finally {
setBusy(false)
}
@@ -94,7 +98,6 @@ export default function NewAnalysis() {
return (
-
{/* Step 1 — Analysis Type */}
@@ -147,7 +150,7 @@ export default function NewAnalysis() {
-
+ {error && }
{/* Submit */}