You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
C.L.E.A.R. — Canadian Lead-Time Early Air Response
A PM2.5 wildfire smoke early warning system
Using air quality monitoring stations 100–600+ km away to provide up to 87 hours of advance warning (24.7 h average) before dangerous smoke arrives in major Canadian cities.
Source of truth: Bui & Zander (2026), Before the Sky Turns Orange: The C.L.E.A.R. System (Canadian Lead-Time Early Air Response)
2003–2023, wildfire season (May–September); 479 stations analyzed (348 NAPS + 131 U.S. EPA), 56 operational after screening
Data volume
36M+ hourly observations from NAPS and U.S. EPA networks
Station Selection Criteria (per Bui & Zander, 2026)
R ≥ 0.30, P < 0.001, N ≥ 100 observations
Toronto reference: NAPS 60430 (primary), 60410 (secondary); highest-station approach
Rule 2 (NW Ontario): Thunder Bay NAPS 60807, 60809 for sequential detection
Architecture
High-Level System Architecture
flowchart TB
subgraph Clients [Clients]
Web[Web Browser]
iOS[iOS App]
API_Client[API Clients]
end
subgraph Vercel [Vercel Serverless]
Django[Django 4.2 App]
Cron[Vercel Cron]
end
subgraph Backend [Backend Services]
Data[data.py - Station Loading]
WAQI[waqi.py - Live PM2.5]
Eval[evaluate.py - 3-Rule Detection]
end
subgraph Storage [Storage]
DB[(Supabase PostgreSQL)]
Cache[(Django Cache / Redis)]
end
subgraph External [External APIs]
WAQI_API[WAQI API]
end
subgraph DataFiles [Data Files]
Excel[Research Excel Files]
NAPS[NAPS Stations.xlsx]
end
Web --> Django
iOS --> Django
API_Client -->|Bearer token| Django
Cron -->|POST /api/refresh/| Django
Django --> Data
Django --> WAQI
Django --> Eval
Data --> Excel
Data --> NAPS
WAQI --> WAQI_API
Eval --> Data
Django --> DB
Django --> Cache
Loading
Component Architecture
flowchart LR
subgraph Webapp [webapp/]
subgraph EWS [ews/ - Django Project]
Settings[settings.py]
URLs[urls.py]
WSGI[wsgi.py]
end
subgraph Dashboard [dashboard/]
Models[models.py]
Views[views/]
Services[services/]
Templates[templates/]
Static[static/]
end
end
subgraph Data [data/]
Research[LAPTOP TSF 2026/]
Config[config.json]
end
WSGI --> Settings
Settings --> Dashboard
Views --> Services
Views --> Models
Services --> Data
Loading
Request Flow
sequenceDiagram
participant User
participant Browser
participant Django
participant Cache
participant DB
participant WAQI
User->>Browser: Open dashboard
Browser->>Django: GET /api/stations/
Django->>Cache: Check 5-min cache
alt Cache hit
Cache-->>Django: Cached stations
else Cache miss
Django->>DB: Load from Excel/bundled
Django->>Cache: Store for 5 min
end
Django-->>Browser: Station list
Browser->>Django: GET /api/live/
Django->>Cache: Check 30s cache
alt Cache hit
Cache-->>Django: Cached response
else Cache miss
Django->>DB: CachedResult (key=latest)
Django->>Cache: Store 30s
end
Django-->>Browser: Live results
erDiagram
User ||--o| UserProfile : has
User ||--o{ APIKey : has
User ||--o{ Suggestion : author
User ||--o{ SuggestionVote : casts
User ||--o{ Comment : author
User ||--o{ DeviceToken : has
User ||--o{ Payment : has
User ||--o{ RefreshToken : has
Suggestion ||--o{ SuggestionVote : has
Suggestion ||--o{ Comment : has
UserProfile {
int id PK
int user_id FK
datetime last_fetch_time
json last_fetch_results
string plan
datetime plan_expires
}
APIKey {
int id PK
int user_id FK
string key UK
string name
int requests_this_hour
datetime hour_started
int total_requests
}
CachedResult {
int id PK
string key UK "latest"
json results
json city_alerts
json readings
datetime timestamp
}
ReadingSnapshot {
int id PK
string city UK
json readings
datetime timestamp
}
Suggestion {
int id PK
int author_id FK
string title
text body
datetime created_at
}
Payment {
int id PK
int user_id FK
string plan
string nowpayments_id UK
string status
}
Loading
Model Summary
Model
Purpose
UserProfile
Plan (free/pro/business), last fetch, rate limits
APIKey
Public API keys, rate limiting per key
CachedResult
Latest evaluation (key='latest'); single row
ReadingSnapshot
Per-city readings for Rule 2 (previous hour)
Suggestion
User feedback board
SuggestionVote
Upvote/downvote on suggestions
Comment
Comments on suggestions
DeviceToken
Push notification tokens (iOS/Android)
Payment
NOWPayments crypto subscriptions
RefreshToken
JWT refresh token (hash stored)
API Reference
Internal APIs (no API key)
Method
Path
Purpose
Cache
GET
/api/stations/
All stations grouped by city
5 min
GET
/api/demo/
Sample ambient-style readings (fallback catalog)
5 min
GET
/api/live/
Latest cached results
30s
POST
/api/refresh/
Cron refresh (Bearer CRON_SECRET)
—
GET
/api/auth-status/
Auth status
—
Public API v1 (requires Authorization: Bearer <API_KEY_OR_JWT>)
Method
Path
Purpose
GET
/api/v1/live/
Current predictions; ?station=<id>, ?limit= (default 50, max 200); data_source is live or demo_preview if cache empty
Create data/config.json (copy from data/config.example.json):
{
"api_key": "YOUR_WAQI_API_KEY_HERE"
}
The app prefers WAQI_API_TOKEN env var over config.json.
Station catalog and live measurements
Station list — The API tries research Excel under data/, then legacy {City}_PM25_EWS_Regression.xlsx, then webapp/dashboard/services/bundled_stations.json when the catalog would otherwise be empty (fresh clone, or Vercel where data/** is not bundled).
Dashboard demo — Uses the same station list as /api/stations/; it does not require WAQI or a populated database cache.
Live view — /api/live/ prefers CachedResult (key=latest) from /api/refresh/. If that row is missing or has no results, the API returns demo-style preview readings (data_source: demo_preview) so the dashboard still shows PM2.5 columns until WAQI is configured.
Seed live cache with demo data (optional) — cd webapp && python manage.py seed_live_demo
Features
Feature
Description
Dashboard
Real-time alert banner, station table, stats cards
Live Map
Interactive Leaflet map with station markers and alert colours
Research
Full research content including methodology, validation
Demo Mode
Sample ambient-style readings when live cache is empty
Auto-fetch
Live data on load and every 15 minutes
Feedback
Suggestion board with voting and comments
API
Public API v1 with JWT and API key auth
Billing
NOWPayments crypto subscriptions
Data Sources
Source
Purpose
NAPS
National Air Pollution Surveillance Program (Environment Canada)
U.S. EPA AQS / AirNow
Border station data
WAQI
World Air Quality Index API for live PM2.5
License & Authors
C.L.E.A.R. — Canadian Lead-Time Early Air Response
Authors: Hugo Bui & Ryan Zander
Institution: University of Toronto Schools
Paper (source of truth): Bui & Zander (2026), Before the Sky Turns Orange: The C.L.E.A.R. System