diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dd7fb7f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,52 @@ +# Git / version control +.git +.gitignore + +# Python +.venv +venv +__pycache__ +*.pyc +*.pyo +*.egg-info +.pytest_cache +.mypy_cache +.coverage +htmlcov + +# Node +frontend/node_modules +frontend/dist +frontend/.env +npm-debug.log* + +# Local data and outputs (mounted at runtime) +data +outputs +logs +models_pretrained + +# Secrets +secrets +.env +.env.* + +# IDE / OS +.claude +.cursor +.vscode +.DS_Store + +# Docs / tests that are not needed at runtime +team_docs +*.pdf +tests +notebooks +scripts/*.sh +scripts/setup_gee.py + +# Large pretrained weights if they exist locally +# (The production ONNX is copied explicitly in the Dockerfile if present) +models/**/*.pth +models/**/*.pt +models/**/best_model.pth diff --git a/.env.example b/.env.example index b4cdc4a..7695471 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,53 @@ # ClimateVision Environment Variables -# Copy this file to .env and fill in your actual credentials -# NEVER commit .env to git! +# Copy this file to .env and fill in your actual credentials. +# NEVER commit .env or secrets/ to git! -# Google Earth Engine +# --------------------------------------------------------------------------- +# Google Earth Engine (required for real satellite imagery) +# --------------------------------------------------------------------------- +# Service account email registered with Earth Engine. GEE_SERVICE_ACCOUNT=your-service-account@your-project.iam.gserviceaccount.com -GEE_PRIVATE_KEY_FILE=path/to/your-private-key.json -# OR if using OAuth: + +# Path to the service-account JSON key, relative to the project root. +# Keep the file in secrets/ (gitignored). +GEE_SERVICE_ACCOUNT_KEY=secrets/gee-key.json + +# Google Cloud / Earth Engine project ID. GEE_PROJECT_ID=your-gee-project-id -# Sentinel Hub (optional - for later) +# --------------------------------------------------------------------------- +# Sentinel Hub (optional future source) +# --------------------------------------------------------------------------- SENTINEL_HUB_CLIENT_ID=your_client_id_here SENTINEL_HUB_CLIENT_SECRET=your_client_secret_here -# API Keys (optional - for later) -API_SECRET_KEY=your_secret_key_here +# --------------------------------------------------------------------------- +# API Security +# --------------------------------------------------------------------------- +# Secret used for API-key hashing and any signed tokens. +# Generate with: openssl rand -hex 32 +API_SECRET_KEY=change_me_in_production + +# Set to 1 ONLY in local development to enable the cv_dev bypass key. +# NEVER enable this in production. +CLIMATEVISION_ALLOW_DEV_KEY=0 + +# --------------------------------------------------------------------------- +# CORS / Frontend +# --------------------------------------------------------------------------- +# Comma-separated list of origins allowed to call the API. +# Include your production domain and any local dev URLs. +CLIMATEVISION_CORS_ORIGINS=https://climatevision.green,https://www.climatevision.green,http://localhost:5173,http://127.0.0.1:5173 + +# --------------------------------------------------------------------------- +# LLM Reporting (optional) +# --------------------------------------------------------------------------- +ANTHROPIC_API_KEY=your_anthropic_api_key +CLIMATEVISION_LLM_MODEL=claude-haiku-4-5-20251001 -# Database (optional - for later) -DATABASE_URL=postgresql://user:password@localhost:5432/climatevision +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- +# SQLite is used by default. Use an absolute path or a project-relative path. +# For production with high traffic, switch to PostgreSQL and update db.py. +DATABASE_URL=sqlite:///outputs/climatevision.sqlite3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 047198f..514a0c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,3 +57,28 @@ jobs: - name: Type check and build run: npm run build + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + target: api + cache-from: type=gha + cache-to: type=gha,mode=max + + # Aggregator job used as a required status check named "ci" + ci: + needs: [python, frontend, docker] + runs-on: ubuntu-latest + steps: + - name: All checks passed + run: echo "CI passed" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..096440d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,23 @@ +name: Deploy + +on: + push: + branches: [main] + +jobs: + test: + uses: ./.github/workflows/ci.yml + + deploy: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Flyctl + uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Deploy to Fly.io + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + run: fly deploy --remote-only diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2d4e1d9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# Multi-stage Dockerfile for ClimateVision +# Builds the React frontend, then packages the FastAPI backend + static files. + +# ----------------------------------------------------------------------------- +# Stage 1: Build the frontend +# ----------------------------------------------------------------------------- +FROM node:20-slim AS frontend-builder + +WORKDIR /app/frontend + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +ENV VITE_API_BASE_URL= +RUN npm run build + +# ----------------------------------------------------------------------------- +# Stage 2: Python API runtime +# ----------------------------------------------------------------------------- +FROM python:3.11-slim AS api + +WORKDIR /app + +# Prevent Python from writing pyc files and buffering stdout +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV OMP_NUM_THREADS=1 + +# Install system dependencies required by rasterio, opencv, and other geospatial libs. +# build-essential and python3-dev are needed to compile packages like stringzilla +# that do not provide pre-built wheels for this platform. +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 \ + libgdal-dev \ + build-essential \ + python3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Install the ClimateVision package +COPY setup.py README.md ./ +COPY src/ ./src/ +RUN pip install --no-cache-dir -e . + +# Copy built frontend from the first stage +COPY --from=frontend-builder /app/frontend/dist ./frontend/dist + +# Copy config and model directory structure +COPY config.yaml ./ +COPY config/ ./config/ +COPY models/ ./models/ + +# Create writable directories for SQLite and outputs +RUN mkdir -p /app/outputs /app/data /app/logs + +EXPOSE 8000 + +# Note: GEE credentials and other secrets are supplied at runtime via env vars. +# Do not bake credentials into the image. +CMD ["uvicorn", "climatevision.api.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..40c7711 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + api: + build: + context: . + dockerfile: Dockerfile + target: api + image: climatevision:latest + container_name: climatevision-api + ports: + - "8000:8000" + env_file: + - .env + volumes: + # Persistent SQLite / outputs + - ./outputs:/app/outputs + # GEE service-account key (read-only) + - ./secrets:/app/secrets:ro + environment: + - DATABASE_URL=sqlite:///app/outputs/climatevision.sqlite3 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..96a20ee --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,169 @@ +# ClimateVision Deployment Guide + +This guide covers deploying ClimateVision to **Fly.io** at `https://climatevision.green` with real Google Earth Engine (GEE) satellite data. + +--- + +## Prerequisites + +- A Fly.io account: https://fly.io +- The Fly CLI (`flyctl`) installed and authenticated +- A Google Cloud project with Earth Engine enabled +- This repository cloned and dependencies installed + +--- + +## 1. Google Earth Engine credentials + +Real satellite imagery requires a GEE service account. + +1. Go to https://console.cloud.google.com/ and select your project. +2. Enable the **Earth Engine API**. +3. Navigate to **IAM & Admin → Service Accounts**. +4. Create a service account with the role **Earth Engine Resource Writer**. +5. Create a JSON key and download it. +6. Place the key at `secrets/gee-key.json` (the `secrets/` directory is gitignored). +7. Register the service account in the Earth Engine console: https://code.earthengine.google.com/ +8. Copy `.env.example` to `.env` and fill in: + ```bash + GEE_SERVICE_ACCOUNT=your-service-account@your-project.iam.gserviceaccount.com + GEE_SERVICE_ACCOUNT_KEY=secrets/gee-key.json + GEE_PROJECT_ID=your-gee-project-id + ``` + +> **Never commit `secrets/` or `.env` to Git.** + +--- + +## 2. Local production-like run (Docker) + +Build and run the full stack locally: + +```bash +# Make sure .env is populated and secrets/gee-key.json exists +docker compose up --build +``` + +The API will be available at http://localhost:8000 and the dashboard at http://localhost:8000/. + +--- + +## 3. Fly.io setup + +### 3.1 Create the app and provision secrets + +```bash +# Create the Fly app +fly apps create climatevision-green + +# Set all secrets from .env +./scripts/setup_fly_secrets.sh + +# Create a persistent volume for SQLite and outputs (1 GB is plenty for demos) +fly volumes create climatevision_data --region lhr --size 1 +``` + +### 3.2 Custom domain + +```bash +# Request a certificate for your domain +fly certs create climatevision.green +``` + +Add the DNS records shown by `fly certs show` to your DNS provider. Once propagated, Fly will serve the app at `https://climatevision.green`. + +### 3.3 Deploy + +```bash +# Manual deploy +fly deploy --remote-only + +# Or push to main — GitHub Actions will deploy automatically +# once FLY_API_TOKEN is configured (see section 4). +``` + +--- + +## 4. GitHub Actions CI/CD + +The repository includes two workflows: + +- `.github/workflows/ci.yml` — runs on every PR/push to `main` or `develop`. +- `.github/workflows/deploy.yml` — deploys to Fly.io on every push to `main`. + +### Required repository secrets + +Add these in **GitHub → Settings → Secrets and variables → Actions**: + +| Secret | Value | +|--------|-------| +| `FLY_API_TOKEN` | A Fly.io access token from https://fly.io/user/personal_access_tokens | + +GEE credentials are **not** stored in GitHub; they are set directly on Fly via `scripts/setup_fly_secrets.sh`. + +--- + +## 5. Verify the deployment + +Check the health endpoint: + +```bash +curl https://climatevision.green/api/health +``` + +Check which models have trained weights: + +```bash +curl https://climatevision.green/api/health/models +``` + +Open the dashboard: + +```bash +open https://climatevision.green +``` + +--- + +## 6. Model status for investor demos + +As of this deployment, only the **flooding** analysis ships with trained weights (`models/flooding_20260513_124208/model.onnx`). + +Deforestation and arctic-ice analyses run using synthetic fallback tiles and untrained weights. The API reports this transparently via `/api/health/models` and the `is_synthetic` flag in prediction responses. + +To add production models: + +1. Train or obtain `models/unet_deforestation.pth` and `models/unet_ice.pth`. +2. Upload them to the `models/` directory. +3. Redeploy. + +--- + +## 7. Alert delivery + +The alert generator emits notifications to pluggable channels. By default only the `log` channel is active. + +To enable email or webhook alerts, register a delivery function in `src/climatevision/inference/alert_generator.py` or implement the alert worker (see open issue #10). + +For email, configure an SMTP provider or a transactional email service such as Mailgun or SendGrid, then set the relevant secrets via `fly secrets set`. + +--- + +## 8. Security checklist + +- [ ] `CLIMATEVISION_ALLOW_DEV_KEY` is `0` in production. +- [ ] `API_SECRET_KEY` is a strong, unique secret. +- [ ] The GEE service-account key is stored only in `secrets/` or Fly secrets, never in Git. +- [ ] Hopelyn’s old PAT has been revoked in GitHub. +- [ ] The Fly app uses HTTPS via `fly certs`. + +--- + +## 9. Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| `GEE tile download failed` | Missing or invalid GEE credentials | Check `.env` / Fly secrets and service-account registration | +| Frontend shows blank page | `frontend/dist` not built or mounted | Re-run `npm run build` in `frontend/` or redeploy | +| CORS errors | Origin not allowed | Add domain to `CLIMATEVISION_CORS_ORIGINS` | +| SQLite data lost on deploy | No Fly volume mounted | Create and mount `climatevision_data` volume on `/app/outputs` | diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..306b22d --- /dev/null +++ b/fly.toml @@ -0,0 +1,26 @@ +app = 'climatevision-green' +primary_region = 'lhr' + +[build] + +[http_service] + internal_port = 8000 + force_https = true + auto_stop_machines = 'off' + auto_start_machines = true + min_machines_running = 1 + processes = ['app'] + +[[mounts]] + source = 'climatevision_data' + destination = '/app/outputs' + +[[vm]] + memory = '2gb' + cpu_kind = 'shared' + cpus = 2 + +[env] + PYTHONUNBUFFERED = '1' + OMP_NUM_THREADS = '1' + DATABASE_URL = 'sqlite:///app/outputs/climatevision.sqlite3' diff --git a/frontend/.env.example b/frontend/.env.example index ffbb571..55304ad 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,8 @@ -# API base URL for frontend requests -# Leave empty when using Vite dev server (proxy handles /api -> backend) -# Set to http://127.0.0.1:8000 when serving built app separately +# ClimateVision frontend environment variables +# Copy to .env and fill in before building for production. + +# Base URL for the ClimateVision API. +# Leave empty for same-origin deployment (e.g. when the API serves the frontend). +# For separate frontend hosting, set the full API origin: +# VITE_API_BASE_URL=https://climatevision.green/api VITE_API_BASE_URL= diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 17fbb71..01661b3 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -3,7 +3,7 @@ */ // API Configuration -export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; export const API_TIMEOUT = 30000; // Map Configuration diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index fe50f31..59defa1 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,16 +1,18 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -export default defineConfig({ +export default defineConfig(({ mode }) => ({ plugins: [react()], server: { port: 5173, strictPort: true, - proxy: { - '/api': { - target: 'http://127.0.0.1:8000', - changeOrigin: true, - }, - }, + proxy: mode === 'development' + ? { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + }, + } + : undefined, }, -}) +})) diff --git a/run_api.sh b/run_api.sh index be1806e..2e51877 100755 --- a/run_api.sh +++ b/run_api.sh @@ -7,18 +7,18 @@ cd "$(dirname "$0")" PORT="${1:-8000}" -if [ ! -d "venv" ]; then - echo "Virtual environment not found. Run: python -m venv venv && source venv/bin/activate && pip install -r requirements.txt && pip install -e ." +if [ ! -d ".venv" ]; then + echo "Virtual environment not found. Run: python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt && pip install -e ." exit 1 fi -source venv/bin/activate +source .venv/bin/activate # Avoid OpenMP sandbox issues; fix NumPy/PyTorch compatibility in spawned workers export OMP_NUM_THREADS=1 -echo "Starting ClimateVision API on http://127.0.0.1:$PORT" -echo " Health: http://127.0.0.1:$PORT/api/health" -echo " API docs: http://127.0.0.1:$PORT/docs" +echo "Starting ClimateVision API on http://0.0.0.0:$PORT" +echo " Health: http://0.0.0.0:$PORT/api/health" +echo " API docs: http://0.0.0.0:$PORT/docs" echo "" -exec uvicorn climatevision.api.main:app --reload --port "$PORT" +exec uvicorn climatevision.api.main:app --host 0.0.0.0 --port "$PORT" --workers 1 diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..1681ec0 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Local deployment helper for Fly.io. +# Usage: ./scripts/deploy.sh + +set -e + +cd "$(dirname "$0")/.." + +echo "Running test suite..." +.venv/bin/pytest tests/ -q + +echo "Building frontend..." +cd frontend +npm ci +VITE_API_BASE_URL= npm run build +cd .. + +echo "Deploying to Fly.io..." +fly deploy --remote-only + +echo "Deployment complete. Check status with: fly status" diff --git a/scripts/setup_fly_secrets.sh b/scripts/setup_fly_secrets.sh new file mode 100755 index 0000000..0096431 --- /dev/null +++ b/scripts/setup_fly_secrets.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Load environment variables from .env and set them as Fly.io secrets. +# Usage: ./scripts/setup_fly_secrets.sh + +set -e + +cd "$(dirname "$0")/.." + +if [ ! -f .env ]; then + echo "Error: .env file not found. Copy .env.example to .env and fill in your credentials." + exit 1 +fi + +# Ensure the GEE key file exists if referenced +GEE_KEY_PATH=$(grep '^GEE_SERVICE_ACCOUNT_KEY=' .env | cut -d '=' -f2- | tr -d '"' | tr -d "'") +if [ -n "$GEE_KEY_PATH" ] && [ ! -f "$GEE_KEY_PATH" ]; then + echo "Warning: GEE_SERVICE_ACCOUNT_KEY points to a missing file: $GEE_KEY_PATH" +fi + +# Read non-empty, non-comment lines and build a single fly secrets set command +SECRETS="" +while IFS= read -r line || [ -n "$line" ]; do + # Skip comments and empty lines + case "$line" in + \#*|""|*\ #*) continue ;; + esac + + # Skip lines without an equals sign + case "$line" in + *=*) ;; + *) continue ;; + esac + + key="${line%%=*}" + value="${line#*=}" + + # Strip surrounding quotes if present + value=$(echo "$value" | sed -e 's/^["'"'"']//' -e 's/["'"'"']$//') + + # Skip empty values + [ -z "$value" ] && continue + + SECRETS="$SECRETS $key=$value" +done < .env + +if [ -z "$SECRETS" ]; then + echo "No non-empty secrets found in .env." + exit 0 +fi + +echo "Setting Fly.io secrets..." +# shellcheck disable=SC2086 +fly secrets set $SECRETS + +echo "Done. Verify with: fly secrets list" diff --git a/setup.py b/setup.py index 9e1b1ce..047d2b5 100644 --- a/setup.py +++ b/setup.py @@ -14,12 +14,12 @@ setup( name="climatevision", - version="0.1.0", + version="0.2.0", author="ClimateVision Contributors", - description="Open-source ML platform for automated deforestation detection using deep learning and satellite imagery", + description="Open-source ML platform for automated environmental monitoring using deep learning and satellite imagery", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/yourusername/ClimateVision", + url="https://github.com/Climate-Vision/ClimateVision", packages=find_packages(where="src"), package_dir={"": "src"}, classifiers=[ @@ -36,7 +36,7 @@ ], python_requires=">=3.8", install_requires=[ - "numpy>=1.21.0", + "numpy>=1.21.0,<2.0.0", "pandas>=1.3.0", "torch>=2.0.0", "torchvision>=0.15.0", @@ -50,6 +50,7 @@ "tqdm>=4.62.0", "pyyaml>=6.0", "requests>=2.26.0", + "python-dotenv>=0.19.0", ], extras_require={ "dev": [ diff --git a/src/climatevision/api/main.py b/src/climatevision/api/main.py index 5c9fb2d..9bcb6fd 100644 --- a/src/climatevision/api/main.py +++ b/src/climatevision/api/main.py @@ -13,6 +13,7 @@ import json import logging +import os import time from datetime import datetime, timezone from pathlib import Path @@ -45,6 +46,7 @@ from climatevision.inference import run_inference_from_file, run_inference_from_gee from climatevision.api.auth import require_api_key from climatevision.governance import explain_prediction, SHAPExplainer +from climatevision.security.api_security import SecurityMiddleware logger = logging.getLogger(__name__) @@ -388,19 +390,30 @@ def create_app() -> FastAPI: ) app.add_middleware(AuditLogMiddleware) + + # CORS: allow local dev origins plus any origins from CLIMATEVISION_CORS_ORIGINS + default_origins = [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://localhost:3000", + "http://127.0.0.1:3000", + ] + extra_origins = [ + origin.strip() + for origin in os.environ.get("CLIMATEVISION_CORS_ORIGINS", "").split(",") + if origin.strip() + ] app.add_middleware( CORSMiddleware, - allow_origins=[ - "http://localhost:5173", - "http://127.0.0.1:5173", - "http://localhost:3000", - "http://127.0.0.1:3000", - ], + allow_origins=list(dict.fromkeys(default_origins + extra_origins)), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + # Wire OWASP-aligned security controls (rate limiting, payload limits, etc.) + app.add_middleware(SecurityMiddleware) + from climatevision.api import admin as _admin app.include_router(_admin.router) @@ -458,6 +471,37 @@ def health() -> dict[str, Any]: "config_issues": config_issues, } + @app.get("/api/health/models") + def health_models() -> dict[str, Any]: + """Report which analysis types have trained weights available on disk.""" + from climatevision.data.band_mapping import get_model_config + + models_status = [] + for atype in SUPPORTED_ANALYSIS_TYPES: + name = atype["name"] + cfg = get_model_config(name) + weights_path = cfg.get("weights") + has_weights = False + if weights_path: + has_weights = (Path(__file__).resolve().parents[3] / weights_path).exists() + models_status.append( + { + "analysis_type": name, + "enabled": atype["enabled"], + "has_trained_weights": has_weights, + "weights_path": weights_path, + "note": "production inference" if has_weights else "synthetic/demo fallback", + } + ) + + ready_count = sum(1 for m in models_status if m["has_trained_weights"]) + return { + "status": "ready" if ready_count else "demo_mode", + "models": models_status, + "ready_count": ready_count, + "total_count": len(models_status), + } + @app.get("/api/analysis-types") def list_analysis_types(enabled_only: bool = True) -> list[dict[str, Any]]: """List available analysis types.""" diff --git a/src/climatevision/config.py b/src/climatevision/config.py index 8265d8c..cb2e211 100644 --- a/src/climatevision/config.py +++ b/src/climatevision/config.py @@ -6,6 +6,12 @@ from typing import Dict, Any import yaml +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass + class Config: """Base configuration class""" diff --git a/src/climatevision/db.py b/src/climatevision/db.py index 711a2ad..04d0349 100644 --- a/src/climatevision/db.py +++ b/src/climatevision/db.py @@ -7,6 +7,7 @@ - Alerts and notifications """ +import os import secrets import sqlite3 from pathlib import Path @@ -23,9 +24,18 @@ def get_db_path() -> Path: """Get the path to the SQLite database file.""" global _DB_PATH if _DB_PATH is None: - db_dir = Config.PROJECT_ROOT / "outputs" - db_dir.mkdir(parents=True, exist_ok=True) - _DB_PATH = db_dir / "climatevision.sqlite3" + db_url = os.environ.get("DATABASE_URL", "") + if db_url.startswith("sqlite:///"): + # Support both absolute paths (sqlite:////...) and project-relative paths + path_part = db_url[len("sqlite:///"):] + if path_part.startswith("/"): + _DB_PATH = Path(path_part) + else: + _DB_PATH = Config.PROJECT_ROOT / path_part + else: + db_dir = Config.PROJECT_ROOT / "outputs" + db_dir.mkdir(parents=True, exist_ok=True) + _DB_PATH = db_dir / "climatevision.sqlite3" return _DB_PATH diff --git a/src/climatevision/inference/pipeline.py b/src/climatevision/inference/pipeline.py index 7af17ab..9ebcd64 100644 --- a/src/climatevision/inference/pipeline.py +++ b/src/climatevision/inference/pipeline.py @@ -19,9 +19,12 @@ from climatevision.data.band_mapping import get_bands_for_analysis, get_model_config from climatevision.models.unet import UNet +from climatevision.security.pipeline_guard import PipelineGuard logger = logging.getLogger(__name__) +_guard = PipelineGuard(enable_input_check=True, enable_output_check=True) + # --------------------------------------------------------------------------- # Project paths (mirrors run_training.py conventions, NOT Config.MODELS_DIR) # --------------------------------------------------------------------------- @@ -217,6 +220,15 @@ def run_inference( ndvi_stats = _compute_ndvi_stats(image) + input_check = _guard.check_input(image) + if input_check.is_anomalous: + logger.warning( + "Anomalous input detected for %s (score=%.4f, type=%s)", + analysis_type, + input_check.anomaly_score, + input_check.anomaly_type, + ) + model, device = _load_model(analysis_type) n_channels = model.n_channels n_classes = model.n_classes @@ -239,6 +251,19 @@ def run_inference( predictions = torch.argmax(output, dim=1) # (1, H, W) probabilities = torch.softmax(output, dim=1) # (1, n_classes, H, W) + output_check = _guard.check_output( + predictions.cpu().numpy(), + probabilities=probabilities.cpu().numpy(), + n_classes=n_classes, + ) + if not output_check.is_valid: + logger.warning( + "Model output validation failed for %s (confidence=%.4f, issues=%s)", + analysis_type, + output_check.confidence, + output_check.issues, + ) + total_pixels = int(predictions.numel()) max_probs = probabilities.max(dim=1).values mean_confidence = float(max_probs.mean().item()) diff --git a/src/climatevision/security/api_security.py b/src/climatevision/security/api_security.py index 7eaa8fc..b4e5e5b 100644 --- a/src/climatevision/security/api_security.py +++ b/src/climatevision/security/api_security.py @@ -32,7 +32,7 @@ class SecurityConfig: """Security configuration settings.""" max_payload_size_bytes: int = 50 * 1024 * 1024 # 50 MB - max_bbox_area_degrees: float = 100.0 # Max area in square degrees + max_bbox_area_degrees: float = 200.0 # Max area in square degrees max_date_range_days: int = 365 # Max date range rate_limit_requests: int = 100 # Requests per window rate_limit_window_seconds: int = 60 # Window size @@ -210,7 +210,7 @@ def validate_payload_size( def validate_bbox( bbox: list[float], - max_area: float = 100.0, + max_area: float = 200.0, ) -> tuple[bool, str]: """ Validate bounding box coordinates.