Skip to content

aykutsp/strainscape

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StrainScape — World Overview

StrainScape

A real-time ambient stress index for cities.
Combines live weather, seismic, air-quality, mobility, macro and news signals into one explainable 0–100 score.

Go 1.22+ MIT License Deploy on Render 12 Live Sources 6 Factors Zero Keys Required

Why · How · Scoring · Screenshots · Run · Deploy · Contribute


Why this exists

Every city has a dozen streams of real-time data — a magnitude 3 event 40 km out, a thunderstorm warning, a stalled motorway, a currency dip, a spike in negative headlines. Individually they're easy to ignore. Stacked together, they shape what a day feels like.

StrainScape turns those streams into one number with an audit trail. Not a mental-health metric, not a prediction — an ambient conditions readout, like a barometer.

A typical request fans out to up to 12 real data sources in parallel — Open-Meteo weather / air quality / pollen / marine / flood, USGS and EMSC earthquake catalogs, NOAA SWPC geomagnetic Kp, NOAA NWS severe weather alerts (US), MeteoAlarm official EU alerts (40+ countries), OpenSky airspace activity, and optionally GDELT 2.0 news tone — plus synthetic fallback providers for traffic, FX and news.

No API keys required. Clone, run, and you get a working score with real live data immediately.


Screenshots

New York — Elevated Tel Aviv — High
New York Tel Aviv
Paris — Elevated World Overview
Paris World

How it works

 request ──▶ geocode ──▶ providers (parallel) ──▶ factor scorers ──▶ dynamic weights ──▶ ASI ──▶ explanation
  1. The HTTP layer resolves a city or coordinate pair via Open-Meteo geocoding.
  2. The provider aggregator fans out to 12 sources in parallel. Each one is non-fatal — failures degrade to neutral defaults and are flagged in the response.
  3. Six factor scorers each produce a 0–100 sub-score with human-readable drivers.
  4. A dynamic weighting pass boosts whichever factor is clearly dominant (active hazard, war zone, extreme heat), then renormalizes weights to 1.0.
  5. The final score, its level band, and an analyst-style explanation are rendered to HTML or served as JSON.

Everything runs in a single Go binary (~12 MB) with templates and static assets embedded. No external cache, no background workers.


Scoring

Factor Base Weight Signals
Hazard Load 0.25 Seismic (USGS + EMSC), severe weather alerts (WMO / NWS / MeteoAlarm), GloFAS river discharge, rough seas, geomagnetic Kp, geopolitical country risk
Environmental Load 0.20 Thom discomfort index, apparent temperature, wind, rain, European AQI, PM2.5, UV index, pollen (CAMS)
Mobility Friction 0.15 Traffic congestion, commute delay, rush-hour timing, airspace activity (OpenSky)
Information Pressure 0.15 News negativity and volume — GDELT 2.0 when enabled, deterministic synthetic fallback otherwise, geopolitical floor
Economic Pressure 0.10 Short-term FX volatility and inflation pressure (per-country tiers)
Context Pressure 0.15 Rush-hour / late-night timing, clustering amplification

Scores land in five bands: Calm (0–24) · Elevated (25–44) · High (45–64) · Intense (65–79) · Critical (80–100).

Dynamic weighting boosts hazard + information for active conflict zones, mobility during rush-hour congestion, and environmental during extreme temperatures. Full model: docs/scoring-model.md.


Data sources

Provider Type What it feeds
Open-Meteo Weather Live Temperature, humidity, wind, precipitation, WMO alerts, local hour
Open-Meteo Air Quality Live PM2.5, PM10, European AQI, UV index
Open-Meteo Pollen (CAMS) Live Grass, birch, alder pollen (Europe)
Open-Meteo Marine Live Wave height (coastal cities)
Open-Meteo Flood (GloFAS) Live River discharge anomaly
USGS Earthquake Catalog Live Seismic events, magnitude, distance
EMSC Seismological Centre Live EU/Med earthquakes (USGS complement)
NOAA SWPC Live Planetary K index (geomagnetic storms)
NOAA NWS Live Severe weather alerts (US only)
MeteoAlarm (EUMETNET) Live Official EU severe weather alerts (40+ countries)
OpenSky Network Live Airspace activity (mobility proxy)
GDELT 2.0 Live (opt-in) News tone + volume (GDELT_ENABLED=true)
Geopolitical tier Curated Country conflict/tension level (120+ countries)
Traffic Synthetic Congestion model (megacity list + rush-hour curve)
Economic / FX Synthetic Per-country volatility tiers
News Synthetic Baseline news pressure (fallback when GDELT is off)

Every response includes a providers array showing each source as live, synthetic, or unavailable so the UI is always honest about what it saw.

Full details: docs/data-sources.md.


Example output

GET /api/score?city=Tel+Aviv

{
  "location": {
    "city": "Tel Aviv",
    "country": "Israel",
    "countryCode": "IL",
    "latitude": 32.0853,
    "longitude": 34.7818,
    "timezone": "Asia/Jerusalem"
  },
  "score": 61.2,
  "level": "High",
  "factors": [
    {
      "key": "hazard",
      "label": "Hazard Load",
      "score": 73.6,
      "weight": 0.34,
      "contribution": 25.0,
      "drivers": ["active severe weather alert", "active armed conflict in country"],
      "narrative": "Recent natural-hazard signals are materially raising background pressure."
    }
  ],
  "explanation": {
    "headline": "Tel Aviv, Israel — multiple signals stacking into a noticeably tense atmosphere right now.",
    "topDrivers": [
      "Hazard Load — active severe weather alert and active armed conflict in country",
      "Information Pressure — coverage dominated by active conflict",
      "Mobility Friction — 89% congestion and +35 min typical delay"
    ]
  },
  "providers": [
    { "name": "open-meteo", "status": "live" },
    { "name": "usgs-earthquake", "status": "live" },
    { "name": "emsc-earthquake", "status": "live" },
    { "name": "noaa-swpc", "status": "live" },
    { "name": "opensky", "status": "live" },
    { "name": "geopolitical-tier", "status": "synthetic" }
  ]
}

Run it

From source

git clone https://github.com/aykutsp/strainscape
cd strainscape
go run .
# Open http://localhost:8080

Tests

go test ./... -race

Docker

docker build -t strainscape .
docker run --rm -p 8080:8080 strainscape

Deploy

A render.yaml is committed at the repo root. To put this on the public internet:

  1. Push the repo to GitHub.
  2. In Render, create a new Blueprint and point it at the repo.
  3. Render picks up render.yaml, builds the Dockerfile, and publishes with a /healthz check.

The same image runs on Fly.io, Railway, Google Cloud Run, or any container host. The only required config is PORT (which every platform sets for you).

Variable Purpose
STRAINSCAPE_CACHE_TTL Snapshot cache TTL (seconds or Go duration)
STRAINSCAPE_HTTP_TIMEOUT Per-provider request timeout
TOMTOM_API_KEY Enables real traffic adapter
EXCHANGERATE_API_KEY Enables real FX / macro adapter
NEWSAPI_KEY Enables real news adapter
GDELT_ENABLED true to enable GDELT 2.0 (rate-limited)

None are required. Weather and seismic paths hit Open-Meteo and USGS with no auth.


Project layout

strainscape/
├── main.go                          # entry point, embeds templates + static
├── internal/
│   ├── domain/                      # shared types (Signals, FactorScore, …)
│   ├── config/                      # weights, level bands, env loading
│   ├── scoring/                     # pure scoring engine (one file per factor)
│   │   ├── hazard.go
│   │   ├── environmental.go
│   │   ├── mobility.go
│   │   ├── information.go
│   │   ├── economic.go
│   │   ├── context.go
│   │   ├── weights.go               # dynamic weight adjustment
│   │   ├── explain.go               # analyst-style explanation builder
│   │   └── engine.go                # orchestrator
│   ├── providers/                   # 18 adapter files (geocoding to geopolitical)
│   ├── cache/                       # TTL in-memory snapshot store
│   └── server/                      # HTTP routes, template funcs, middleware
├── web/
│   ├── templates/index.html         # server-rendered UI
│   └── static/                      # CSS + ~120 lines vanilla JS (autocomplete, geolocation)
├── docs/
│   ├── architecture.md
│   ├── scoring-model.md
│   ├── data-sources.md
│   ├── research-notes.md
│   └── screenshots/
├── Dockerfile                       # two-stage distroless build
├── render.yaml
├── Makefile
└── CONTRIBUTING.md

Key design decisions

  • Everything is explicit. No trained model, no ML inference, no hidden state. Weights, band boundaries and factor logic are all in source. If a score surprises you, the cause is one go test run away.
  • Providers are adapters. Each one talks to its API, normalizes to Signals, returns a status. The scoring engine is pure and never touches the network.
  • Failure is first-class. The providers array shows which sources were live, synthetic, or unavailable, so the UI is always honest.
  • Geopolitical realism. A curated per-country conflict tier ensures active war zones (Ukraine, Gaza, Yemen, Sudan) consistently outscore peaceful megacities regardless of the day's synthetic news baseline.
  • One binary, zero deps. The Go binary embeds templates and static assets. No external cache, no build step for the frontend, no Node.js.

Contributing

Pull requests welcome. Good first changes:

  • Real adapters for traffic, FX, and news (replacing synthetic fallbacks)
  • Additional factor families (noise, air traffic delays, public transit reliability)
  • More scenario tests in internal/scoring/engine_test.go
  • Historical trend endpoint backed by a persistence layer

See CONTRIBUTING.md and docs/architecture.md.


Disclaimer

StrainScape estimates ambient environmental conditions around a location. It is not a medical device, not a diagnostic tool, and not a measurement of any individual's psychological state. Treat the score as a barometer, not a verdict.


MIT License · Scoring Model · Data Sources · Architecture

About

A real-time ambient stress index for cities — 12+ live data sources, zero API keys required

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors