Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e1a0335
Merge pull request #3 from Climate-Vision/feature/api-middleware-audit
Goldokpa Mar 28, 2026
4bddcb3
Merge pull request #4 from Climate-Vision/feature/analytics-statistics
Goldokpa Mar 28, 2026
cea2e6a
docs: add Francis Umo role documentation
Mar 31, 2026
d37cbe7
docs: add Olufemi Taiwo role documentation
Mar 31, 2026
319a88f
Merge pull request #5 from Climate-Vision/docs/francis-role-document
Goldokpa Mar 31, 2026
326f3eb
Merge pull request #6 from Climate-Vision/docs/femi-role-document
Goldokpa Mar 31, 2026
0f4a362
feat(data): GEE tile downloader, band mapping, and cloud masking (#7)
Oshgig Apr 15, 2026
51edfac
feat(inference): make pipeline analysis-aware with dynamic model load…
Oshgig Apr 15, 2026
f2b9373
Merge develop into main: data pipeline + analysis-aware inference
femi23 Apr 15, 2026
1257e7a
feat(api): enforce API key auth with dev bypass, surface is_synthetic…
femi23 Apr 19, 2026
256fbf6
ci: add pytest scaffolding and GitHub Actions workflow
Apr 19, 2026
139ed61
test(models): add UNet and Siamese architecture tests
Godswill-code Apr 19, 2026
0da6c79
docs: add first-time and intermediate contributor issue guides
Goldokpa Apr 19, 2026
ff21090
fix(frontend): correct case-sensitive import paths for Map components
Apr 19, 2026
cf96100
fix(pipeline): remove unnecessary global declaration causing flake8 F824
femi23 Apr 19, 2026
c3d02c1
ci: install system deps before pip install (GDAL, OpenGL)
Apr 19, 2026
f7a7564
ci: remove redundant gdal pip package and simplify system deps
Apr 19, 2026
7c317df
ci: install package in editable mode for pytest
Apr 19, 2026
b8e34ea
feat(data): add dataset, augmentation, and synthetic data modules
Apr 19, 2026
aa643ea
fix(deps): add email-validator for pydantic EmailStr support
Apr 19, 2026
6ac29d1
docs: update Victor's role doc with sprint progress and live CI config
Apr 19, 2026
2c5a486
Add reusable ApiError component for prediction and upload failures
liascope May 8, 2026
b29fbfc
fix: API error handling + formatting
liascope May 16, 2026
b5341d2
fix: update ApiError formatting
liascope May 18, 2026
46db721
style: resolve formatting conflict
liascope May 18, 2026
76d543a
style: align formatting across components
liascope May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ ENV/
!notebooks/*.ipynb

# Data
data/
datasets/
/data/
/datasets/
*.tif
*.tiff
*.h5
Expand Down
28 changes: 27 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/ui/ApiError.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex bg-red-950/90 items-start justify-between overflow-hidden gap-3 rounded-xl sm:text-xs md:text-base border border-red-500/70 p-3 text-white/80">
<AlertCircle className="mt-0.5 h-5 w-5 shrink-0" />

<div className="flex-1 text-sm break-words">{message}</div>

<button type="button" aria-label="Dismiss" onClick={() => setVisible(false)} className="text-white/50 hover:text-red-900">
<X className="h-4 w-4" />
</button>
</div>
)
}
23 changes: 11 additions & 12 deletions frontend/src/pages/NewAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -33,6 +34,7 @@ function SectionLabel({ step, label }: { step: number; label: string }) {
}

export default function NewAnalysis() {
const [error, setError] = useState<string | null>(null)
const { showToast } = useToast()
const { googleMapsApiKey } = useApp()
const navigate = useNavigate()
Expand Down Expand Up @@ -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)
Expand All @@ -85,16 +87,17 @@ 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)
}
}

return (
<div className="max-w-4xl mx-auto px-6 py-8 space-y-8">

{/* Step 1 — Analysis Type */}
<section>
<SectionLabel step={1} label="Analysis Type" />
Expand Down Expand Up @@ -147,7 +150,7 @@ export default function NewAnalysis() {
</div>
</div>
</section>

{error && <ApiError message={error} />}
{/* Submit */}
<button
onClick={handleSubmit}
Expand All @@ -166,11 +169,7 @@ export default function NewAnalysis() {
</button>

{/* Inline hint if bbox not set */}
{!bbox && (
<p className="text-xs text-cv-text-dim text-center -mt-4">
Draw a region on the map above to enable prediction
</p>
)}
{!bbox && <p className="text-xs text-cv-text-dim text-center -mt-4">Draw a region on the map above to enable prediction</p>}

{/* Results */}
{resultRun && (
Expand All @@ -179,7 +178,7 @@ export default function NewAnalysis() {
<ErrorBoundary section="Results">
<ResultsPanel
run={resultRun}
payload={resultPayload as Record<string, unknown> & { inference?: Record<string, number> } | null}
payload={resultPayload as (Record<string, unknown> & { inference?: Record<string, number> }) | null}
onRunAgain={() => {
setResultRun(null)
setResultPayload(null)
Expand Down
27 changes: 19 additions & 8 deletions frontend/src/pages/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { CloudUpload, FileText, X, ChevronDown, ChevronUp, Loader2 } from 'lucid
import type { AnalysisType } from '../api'
import { predictUpload } from '../api'
import { AnalysisTypeSelector } from '../components/ui/AnalysisTypeSelector'
import { MapBBoxPicker } from '../components/map/MapBBoxPicker'
import { MapBBoxPicker } from '../components/Map/MapBBoxPicker'
import { ErrorBoundary } from '../components/ui/ErrorBoundary'
import { useToast } from '../contexts/ToastContext'
import { useApp } from '../contexts/AppContext'
import { ApiError } from '../components/ui/ApiError'

const ACCEPTED = ['.tif', '.tiff', '.geotiff', '.nc', '.hdf5']
const MAX_MB = 500
Expand All @@ -18,6 +19,7 @@ function formatBytes(bytes: number) {
}

export default function Upload() {
const [error, setError] = useState<string | null>(null)
const { showToast } = useToast()
const { googleMapsApiKey } = useApp()
const navigate = useNavigate()
Expand Down Expand Up @@ -55,10 +57,10 @@ export default function Upload() {
}, [])

const handleUpload = async () => {
if (!file) return
if (!file) return
setBusy(true)
setUploadProgress(0)

setError(null)
try {
const res = await predictUpload({
file,
Expand All @@ -75,17 +77,25 @@ export default function Upload() {
})
setFile(null)
setUploadProgress(null)
} catch (e) {
showToast('error', String(e))
setUploadProgress(null)
} finally {
} catch (e: any) {
const message =
e?.response?.data?.detail ||
e?.response?.data?.message ||
e?.message ||
'Upload failed';

setError(message)
showToast('error', message)

setUploadProgress(null);
}
finally {
setBusy(false)
}
}

return (
<div className="max-w-3xl mx-auto px-6 py-8 space-y-6">

{/* Drop Zone */}
<div
onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
Expand Down Expand Up @@ -186,6 +196,7 @@ export default function Upload() {
</div>

{/* Upload button */}
{error && <ApiError message={error} />}
<button
onClick={handleUpload}
disabled={!file || busy}
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ scikit-learn>=1.0.0

# Geospatial Data Processing
rasterio>=1.3.0
gdal>=3.4.0
geopandas>=0.12.0
shapely>=2.0.0
pyproj>=3.4.0
Expand Down Expand Up @@ -40,6 +39,7 @@ dask[complete]>=2023.1.0
fastapi>=0.95.0
uvicorn[standard]>=0.20.0
pydantic>=2.0.0
email-validator>=2.0.0
python-multipart>=0.0.5

# MLOps (optional)
Expand Down
Loading