Skip to content

miruns/ambient-scan

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌍 Ambiant Scan

Zero-Dependency single-file Environmental Scraper & Modeler
Part of The Governor HQ Suite

A single-file Node.js server that scrapes legitimate free APIs and returns normalized environmental data: temperature, air quality, UV index, humidity, wind, atmospheric pressure, geolocation, and more.

Features

  • Zero dependencies β€” just node server.js
  • GPS β†’ City β†’ Cache β†’ Query pipeline for performance
  • GeoIP endpoint β€” auto-detect caller position from IP (proxy-aware)
  • 4-tier LRU cache with TTL (geo, city-resolve, data, geoip)
  • Coordinate grid snapping (~1.1km) β€” nearby requests share cache entries
  • Parallel API fetching β€” weather + air quality fetched simultaneously
  • Graceful degradation β€” returns partial data if one source is down
  • Fly.io ready β€” respects Fly-Client-IP, X-Forwarded-For, X-Real-IP
  • CORS enabled β€” query from any frontend

Data Sources

Source Data API Key
Open-Meteo Weather, UV, Air Quality ❌ Not required
BigDataCloud Reverse Geocoding ❌ Not required
ip-api.com GeoIP (IP β†’ location) ❌ Not required

Quick Start

cd ambiant-scan
node server.js

Server starts on http://localhost:3400

API Collections for fast testing are included β€” see API Collections below.

API

GET /scan?lat=XX&lon=YY

Scan by GPS coordinates.

curl "http://localhost:3400/scan?lat=45.50&lon=-73.57"

GET /scan?city=NAME

Scan by city name.

curl "http://localhost:3400/scan?city=Montreal"

GET /geoip

Returns the caller's geolocation based on their IP address. Handles reverse proxies automatically.

curl "http://localhost:3400/geoip"

Behind a proxy / Fly.io:

# Simulating Fly.io header locally:
curl -H "Fly-Client-IP: 24.48.0.1" "http://localhost:3400/geoip"
Example response
{
  "ip": "24.48.0.1",
  "lat": 45.6085,
  "lon": -73.5493,
  "city": "Montreal",
  "region": "Quebec",
  "regionCode": "QC",
  "country": "Canada",
  "countryCode": "CA",
  "zip": "H1K",
  "timezone": "America/Toronto",
  "isp": "Le Groupe Videotron Ltee",
  "org": "Videotron Ltee",
  "as": "AS5769 Videotron Ltee",
  "source": "fly-client-ip",
  "timestamp": "2026-02-18T20:36:13.181Z"
}

IP Resolution Priority:

Priority Header Set by
1 Fly-Client-IP Fly.io edge proxy
2 X-Forwarded-For (first entry) Most reverse proxies
3 X-Real-IP Nginx
4 socket.remoteAddress Direct connection

On localhost, returns a helpful warning since private IPs can't be geolocated.

GET /health

Health check.

GET /cache/stats

View cache hit rates and entry counts for all 4 caches.

DELETE /cache

Flush all caches.

Response Shape (/scan)

{
  "meta": {
    "source": "ambiant-scan",
    "version": "1.0.0",
    "timestamp": "2026-02-18T12:00:00.000Z",
    "location": {
      "city": "Montreal",
      "region": "Quebec",
      "country": "Canada",
      "countryCode": "CA",
      "coordinates": { "lat": 45.5, "lon": -73.57 }
    },
    "timezone": "America/Toronto",
    "elevation_m": 36,
    "_cached": false,
    "_responseTime_ms": 287
  },
  "temperature": {
    "current_c": -8.2,
    "feels_like_c": -14.1,
    "daily_high_c": -5.0,
    "daily_low_c": -12.3,
    "unit": "Β°C"
  },
  "air_quality": {
    "us_aqi": 42,
    "level": "good",
    "concern": "Air quality is satisfactory",
    "pollutants": {
      "pm2_5": { "value": 8.1, "unit": "ΞΌg/mΒ³" },
      "pm10": { "value": 12.4, "unit": "ΞΌg/mΒ³" },
      "...": "..."
    }
  },
  "uv_index": {
    "current": 1.2,
    "clear_sky": 1.8,
    "daily_max": 2.5,
    "level": "low",
    "concern": "No protection needed"
  },
  "humidity": { "relative_percent": 72 },
  "wind": {
    "speed_kmh": 15.3,
    "gusts_kmh": 28.1,
    "direction_degrees": 225,
    "direction_label": "SW",
    "description": "gentle breeze"
  },
  "atmosphere": {
    "pressure_msl_hpa": 1018.2,
    "surface_pressure_hpa": 1014.1,
    "cloud_cover_percent": 75
  },
  "precipitation": {
    "current_mm": 0,
    "rain_mm": 0,
    "daily_sum_mm": 2.1,
    "daily_probability_percent": 45
  },
  "conditions": {
    "weather_code": 2,
    "description": "Partly cloudy",
    "is_day": true
  },
  "sun": {
    "sunrise": "2026-02-18T07:02",
    "sunset": "2026-02-18T17:31"
  }
}

Configuration

Variable Default Description
PORT 3400 Server port
CACHE_TTL_SECONDS 600 Environmental data cache TTL (10 min)
GEO_CACHE_TTL_SECONDS 86400 Geocoding cache TTL (24 hours)
MAX_CACHE_ENTRIES 5000 Max entries per cache before LRU eviction

Cache Architecture

Request
  β”œβ”€β”€ /geoip
  β”‚     └── geoipCache (24h TTL) β†’ IP geolocation
  β”‚
  β”œβ”€β”€ ?city=Montreal
  β”‚     └── cityResolveCache (24h TTL) β†’ lat/lon
  β”‚
  β”œβ”€β”€ ?lat=45.5&lon=-73.6
  β”‚     └── roundCoords (~1.1km grid snap)
  β”‚           └── geoCache (24h TTL) β†’ city metadata
  β”‚
  └── coordsKey β†’ dataCache (10min TTL)
        β”œβ”€β”€ HIT β†’ return instantly
        └── MISS β†’ parallel fetch [weather + air quality]
                     β†’ model β†’ cache β†’ return

API Collections

Pre-built collection files for fast testing are included in the collections/ folder:

File Client
collections/insomnia.json Insomnia β€” Import via Application β†’ Import
collections/postman.json Postman β€” Import via File β†’ Import
collections/api.http VS Code REST Client / JetBrains HTTP Client

All collections use a base_url variable (default http://localhost:3400) so you can switch between local and production.

Deployment (Fly.io)

fly launch        # first time
fly deploy        # subsequent deploys

The included fly.toml is pre-configured. The /geoip endpoint works automatically on Fly.io β€” the Fly-Client-IP header is set by the edge proxy.

License

MIT

About

Zero-dependency single-file server for normalized environmental data: temperature, air quality, UV index, humidity, wind, and geolocation.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors