SAANS — Smog Advisory & School Closure Decision System

00 — Project Overview & System Architecture

Read this file first. This is the master spec. Files 01–04 in this folder are phase-by-phase build instructions that assume you have read this document. Build in the order: 00 → 01 → 02 → 03 → 04.

Code review notice (applies to every file in this set): All code produced from these instructions will be reviewed by Claude Code before it is accepted into the repo. Write clean, modular, well-commented, efficient code from the start:

  • No dead code, no commented-out blocks, no placeholder TODOs left unexplained.
  • Small, single-responsibility functions/modules — avoid 300-line files.
  • Consistent naming, typed where the language supports it (TypeScript types / Python type hints / Pydantic models).
  • Every API endpoint documented (docstring or OpenAPI annotation) with inputs, outputs, and error cases.
  • No hardcoded secrets/API keys — use .env + .env.example.
  • Include basic error handling and input validation on every external call (AQI APIs, WhatsApp, Twilio, DB).
  • Write code assuming a stricter reviewer will read every line and reject sloppy work — optimize for clarity over cleverness.

1. Project Identity

  • Name: SAANS ("breath" in Urdu)
  • Goal: Give individual Lahore schools a daily, location-specific smog risk score (Green/Amber/Red) and a concrete recommended action, distributed via web, WhatsApp, and printable PDF — replacing blanket province-wide closure orders with targeted, timely, explainable guidance.
  • Hackathon constraint: Must be fully buildable and demoable within ~36 hours. Every architectural choice below is deliberately the simplest thing that works, not the most sophisticated thing possible.

2. High-Level System Architecture

                         ┌─────────────────────────────┐
                         │   External Data Sources       │
                         │  - WAQI / aqicn.org API       │
                         │  - OpenAQ v3 API               │
                         │  - Open-Meteo (wind/forecast)  │
                         │  - OSM Overpass (school seed)  │
                         └───────────────┬───────────────┘
                                         │
                         ┌───────────────▼───────────────┐
                         │      SCORING ENGINE (cron)     │
                         │  - nearest-station lookup      │
                         │  - interpolation fallback      │
                         │  - wind/time-of-day adjustment │
                         │  - tier classification         │
                         │  - confidence indicator        │
                         └───────────────┬───────────────┘
                                         │  writes
                         ┌───────────────▼───────────────┐
                         │        DATABASE (Postgres/     │
                         │         SQLite for demo)       │
                         │  schools | readings | scores    │
                         │  subscribers | broadcast_log    │
                         └───────────────┬───────────────┘
                                         │  reads
              ┌──────────────────────────┼──────────────────────────┐
              │                          │                          │
   ┌──────────▼─────────┐   ┌────────────▼───────────┐   ┌──────────▼─────────┐
   │   BACKEND API        │   │   PDF BULLETIN JOB      │   │  WHATSAPP/SMS BOT   │
   │  FastAPI or Node/    │   │  headless HTML→PDF      │   │  WhatsApp Business  │
   │  Express (REST)      │   │  (Puppeteer)            │   │  API sandbox /      │
   │                      │   │                          │   │  Twilio             │
   └──────────┬───────────┘   └────────────┬────────────┘   └──────────┬─────────┘
              │                            │                          │
   ┌──────────▼───────────┐                │                          │
   │  WEB DASHBOARD         │               │                          │
   │  React + Leaflet map   │               │                          │
   │  search + trend view   │               │                          │
   └────────────────────────┘   ┌───────────▼──────────┐   ┌───────────▼─────────┐
                                 │  Posted at school gate │   │  Parent/school       │
                                 │  (printable PDF)       │   │  WhatsApp groups     │
                                 └─────────────────────────┘   └─────────────────────┘

3. Core Components (map to Phase files)

# Component Phase file Owner role (from proposal)
1 Data ingestion + Scoring engine + DB schema 01_PHASE1_BACKEND_DATA_SCORING.md Data/backend engineer
2 Web dashboard (search, map, trend) 02_PHASE2_FRONTEND_DASHBOARD.md Frontend engineer
3 WhatsApp bot, SMS fallback, PDF bulletin generator 03_PHASE3_DISTRIBUTION_WHATSAPP_PDF_SMS.md Integrations engineer
4 Timeline, milestones, demo script, pitch deck 04_MILESTONES_TIMELINE_TEAM.md Product/pitch lead

4. Technology Stack (final decisions — do not re-litigate mid-build)

Layer Choice Why
Backend API Python + FastAPI (preferred) or Node/Express if team is stronger in JS Async-friendly, auto OpenAPI docs, fast to scaffold
Database SQLite for hackathon demo, schema written to be Postgres-compatible Zero setup, portable, upgrade path is trivial
ORM SQLAlchemy (Python) or Prisma (Node) Type-safe queries, easy migrations
Scheduler APScheduler (Python) or node-cron (Node) In-process cron, no external infra needed
Frontend React + Vite + TypeScript, Leaflet.js for map, TailwindCSS for styling Fast dev loop, small bundle, good map ecosystem
PDF generation Puppeteer (headless Chrome) rendering an HTML template Reuses dashboard's HTML/CSS, no separate PDF templating language
WhatsApp WhatsApp Business Platform (Cloud API) sandbox Free tier, official, well-documented
SMS fallback Twilio Simple REST API, generous trial credit
Hosting (demo) Render / Railway / Fly.io for backend, Vercel/Netlify for frontend Free tiers, fast deploy, good for a 36-hr build
Version control GitHub, one repo, trunk-based with short-lived feature branches Required deliverable is a GitHub repo

5. Repository Structure (create this exact skeleton on day 0)

saans/
├── backend/
│   ├── app/
│   │   ├── main.py                 # FastAPI app entrypoint
│   │   ├── config.py                # env var loading
│   │   ├── db/
│   │   │   ├── models.py            # SQLAlchemy models
│   │   │   └── session.py
│   │   ├── ingestion/
│   │   │   ├── waqi_client.py
│   │   │   ├── openaq_client.py
│   │   │   └── openmeteo_client.py
│   │   ├── scoring/
│   │   │   ├── engine.py            # core scoring logic
│   │   │   ├── tiers.py             # tier thresholds + labels
│   │   │   └── confidence.py        # distance-based confidence calc
│   │   ├── schools/
│   │   │   ├── registry_loader.py   # Overpass ingestion + manual seed merge
│   │   │   └── seed_schools.json
│   │   ├── api/
│   │   │   ├── routes_schools.py
│   │   │   ├── routes_scores.py
│   │   │   └── routes_subscribers.py
│   │   └── jobs/
│   │       └── scheduler.py         # 6 AM + midday refresh jobs
│   ├── tests/
│   ├── requirements.txt
│   └── .env.example
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── MapView.tsx
│   │   │   ├── SchoolSearch.tsx
│   │   │   ├── ScoreCard.tsx
│   │   │   └── TrendChart.tsx
│   │   ├── api/client.ts
│   │   ├── pages/Dashboard.tsx
│   │   └── main.tsx
│   ├── package.json
│   └── .env.example
├── distribution/
│   ├── whatsapp/
│   │   ├── bot.py                   # webhook + broadcast sender
│   │   └── message_templates.py
│   ├── sms/
│   │   └── twilio_sender.py
│   └── pdf/
│       ├── bulletin_template.html
│       └── generate_bulletin.py     # Puppeteer/pyppeteer wrapper
├── docs/
│   ├── ARCHITECTURE.md              # copy of this file
│   ├── API_CONTRACT.md
│   └── demo_script.md
├── .github/workflows/ci.yml         # lint + test on push
└── README.md

6. Data Model (authoritative — implement exactly this in Phase 1)

schools | field | type | notes | |---|---|---| | id | UUID/int PK | | | name | string | | | zone | string | e.g. DHA, Model Town, Gulberg, Township, Batapur-adjacent | | lat, lon | float | | | source | enum(overpass,manual) | |

aqi_readings | field | type | notes | |---|---|---| | id | PK | | | station_id | string | from WAQI/OpenAQ | | lat, lon | float | station location | | aqi_value | int | | | pm25 | float nullable | | | recorded_at | datetime | | | source | enum(waqi,openaq) | |

scores | field | type | notes | |---|---|---| | id | PK | | | school_id | FK → schools | | | score_date | date | | | computed_at | datetime | | | raw_aqi | float | interpolated or nearest-station value | | adjusted_aqi | float | after wind/time-of-day adjustment | | tier | enum(green,amber,red) | | | recommendation | string | human-readable action text | | confidence | enum(high,medium,low) | based on distance to nearest station | | distance_to_station_km | float | |

subscribers | field | type | notes | |---|---|---| | id | PK | | | school_id | FK → schools nullable | null = neighbourhood-level subscriber | | channel | enum(whatsapp,sms) | | | contact | string | phone number | | opted_in_at | datetime | |

broadcast_log | field | type | notes | |---|---|---| | id | PK | | | subscriber_id | FK | | | score_id | FK | | | sent_at | datetime | | | status | enum(sent,failed) | |

7. Tier Logic (must match proposal exactly)

Tier AQI Range Action
Green — Normal 0–100 Outdoor activity, sports, recess proceed as normal
Amber — Caution 101–200 Move recess/sports indoors; sensitive students avoid outdoor exposure
Red — High Risk 201+ Recommend remote learning or indoor-only day; flag to admin for closure decision

Implement thresholds as named constants in scoring/tiers.py, not magic numbers scattered across the codebase.

8. Non-Negotiable Constraints for the Build

  1. Every AQI/weather API call must have a timeout, retry-once, and graceful fallback (use last-known-good cached reading rather than crashing).
  2. The system must never present an interpolated score as if it were a direct sensor reading — the confidence field is mandatory on every score object returned by the API.
  3. No feature (WhatsApp, SMS, PDF) should block the core deliverable (web dashboard with real scores). Build in the priority order defined in file 04_MILESTONES_TIMELINE_TEAM.md.
  4. All official/health-authority language must stay hedged: SAANS is a decision-support aid, not a replacement for Punjab EPA/health authority guidance. This applies to UI copy, WhatsApp message templates, and the PDF bulletin footer.

9. Environment Variables (define in .env.example at repo root and per-service)

WAQI_API_TOKEN=
OPENAQ_API_KEY=
OPENMETEO_BASE_URL=https://api.open-meteo.com/v1
DATABASE_URL=sqlite:///./saans.db
WHATSAPP_CLOUD_API_TOKEN=
WHATSAPP_PHONE_NUMBER_ID=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
FRONTEND_BASE_URL=http://localhost:5173
BACKEND_BASE_URL=http://localhost:8000

Next: proceed to 01_PHASE1_BACKEND_DATA_SCORING.md.

Built With

Share this project:

Updates