COWX (COlorado + Weather) aggregates weather and air-quality data for Colorado locations, built as a static site on GitHub Pages. A scheduled fetch job merges public data sources into JSON under public/data/; the browser app geo-locates you to the nearest site and offers deep drill-down panels.
Scope: Colorado only. Unofficial project — not affiliated with NWS, NOAA, or other government agencies.
Requirements: Node 22.19+, pnpm 10+
git clone <repository-url>
cd cowx
pnpm install
pnpm validate:locations
pnpm fetch:data
npx serve publicOpen the URL printed by serve (typically http://localhost:3000). The app loads data from public/data/.
Without running pnpm run fetch:data, only committed snapshot data in public/data/ is available.
| Command | Description |
|---|---|
pnpm fetch:data |
Run the fetch orchestrator; write/update public/data/*.json |
pnpm run fetch:climatology |
Rebuild ERA5 day-of-year normals only (slow; optional CLIMATOLOGY_MAX_LOCS) |
pnpm test |
Unit tests (fixtures only — no live API calls) |
pnpm lint |
ESLint |
pnpm validate:locations |
Validate scripts/locations/colorado-locations.json (+ co-zips.json) |
pnpm validate:schemas |
Ajv-check catalog + sample public/data against schemas/ |
pnpm format |
Prettier |
pnpm update |
Refresh lockfile within ranges (pnpm update && pnpm dedupe) |
Pre-commit hooks (Husky + lint-staged) run ESLint/Prettier on staged files via prepare.
The site is deployed to the gh-pages branch on code pushes to main (.github/workflows/pages.yml, UI only — preserves live data/) and again after each weather fetch (.github/workflows/update-weather.yml, full public/ including weather JSON). Bot data commits alone do not trigger pages.yml (GITHUB_TOKEN recursion guard).
Live site: https://rinchen.github.io/cowx/
Same-repo pull requests get a sticky comment with a live preview URL under /pr-preview/pr-{N}/ (see .github/workflows/preview.yml). Previews deploy the PR’s UI from public/ but omit public/data/ and point the client at production /cowx/data (legacy GitHub Pages rejects a full duplicated data tree). They are removed when the PR closes. Fork PRs do not get automatic previews — use npx serve public locally. Preview URLs share the production GitHub Pages origin — treat them as untrusted until you review the PR.
One-time setup (after the first gh-pages deploy succeeds):
- Settings → Pages → Build and deployment → Source: Deploy from a branch →
gh-pages//(not “GitHub Actions”). - Settings → Actions → General → Workflow permissions: Read and write permissions.
Weather data targets a ~45-minute cadence via .github/workflows/update-weather.yml (staggered :05/:15/:25/:35/:45/:55 crons plus workflow_dispatch). GitHub may delay schedules; the workflow skips fetch/deploy when live CDN meta.json is still fresh (< 40 minutes), and can deploy-only when main is fresh but the CDN is stale. Every 30 minutes, .github/workflows/check-stale-data.yml checks live generatedAt in two tiers: at ≥ 90 minutes it quietly dispatches Update Weather (self-heal) and stays green, and at ≥ 120 minutes it dispatches, waits for live meta to recover, and only Discord-alerts / fails when recovery still has not landed (unreachable meta alerts immediately). Fetch/Pages verify failures also notify. Committed JSON in public/data/ (published by the weather workflow) is what visitors see between runs. public/.nojekyll keeps GitHub Pages from running Jekyll on the static tree.
GitHub scheduled workflows are best-effort and have delayed both the weather workflow and its watchdog for several hours at once. To target a ~2-hour maximum data age, configure an independent 15-minute keepalive:
- Create a fine-grained GitHub personal access token limited to this repository with Actions: Read and write permission. No Contents permission is needed.
- In cron-job.org, create a job that runs every 15 minutes:
- Method:
POST - URL:
https://api.github.com/repos/rinchen/cowx/actions/workflows/check-stale-data.yml/dispatches - Headers:
Authorization: Bearer <fine-grained PAT>Accept: application/vnd.github+jsonX-GitHub-Api-Version: 2022-11-28
- Body:
{"ref":"main","inputs":{"source":"external"}}
- Method:
- Confirm that Check Stale Data appears in Actions with event
workflow_dispatchabout every 15 minutes.
Store the token only in cron-job.org. Never commit it, add it to documentation, or print it in logs. Without the external keepalive, the GitHub schedules remain best-effort and cannot guarantee a two-hour ceiling.
Optional secrets improve inline sensor/AQI data and failure alerting. Configure under Settings → Secrets and variables → Actions. Use these names only — never commit values:
| Secret name | Purpose |
|---|---|
AIRNOW_API_KEY |
EPA AirNow AQI near locations |
COTRIP_API_KEY |
COtrip JSON feed (RWIS, incidents, planned events, road conditions) |
FIRMS_MAP_KEY |
NASA FIRMS VIIRS active-fire detections (free registration) |
NOTIFY_WEBHOOK_URL |
Webhook for Discord (or compatible) alerts on fetch/Pages failure or stale live data (≥ 2h) |
The site works without these keys; affected sources (including NASA FIRMS hotspots when FIRMS_MAP_KEY is unset) degrade to skipped status in meta.json and offsite links in the UI. AirGradient community PM2.5 uses a free public feed (no key). CDOT cameras, ArcGIS road alerts (fallback), CWOP PWS, HMS smoke, SPC fire weather, NIFC nearby fires, CBRFC water-supply guidance, NOAA SWPC space weather, and burn-restriction links need no secrets. City webcam portals are catalog links (new tab), not embedded feeds. For local fetch testing, copy .env.example to .env (gitignored); notify is Actions-only.
- No accounts and no server-side storage of personal data.
- Favorites, last-viewed location, and hyperlocal pin are stored only in your browser (
localStoragekeys such ascowx:favorites,cowx:lastLocation, andcowx:hyperlocalPin). The pin (lat/lon) survives refresh; searching a city clears it. Address query text is not retained after geocoding. - IP geolocation runs in the browser from public CORS geo APIs to suggest the nearest Colorado site. Coordinates are not sent to a COWX backend (there is none).
- No third-party analytics by default.
See how-it-works.html for details.
- ADAPT.md — fork this site for another US state (catalog, adapters, branding)
- AGENTS.md — contributor and agent guide (adapters, locations, CI, a11y)
- How it works — architecture, update cadence, failure modes
- Credits — data sources and attribution
MIT. Third-party data remains subject to each provider’s terms.