Skip to content

Repository files navigation

WaterLevels.org

Rails 8.1 / Ruby 4 app that maps USGS water monitoring locations (water level, flow, temperature) with cached station and state pages.

Stack

  • PostgreSQL, Redis, Sidekiq (+ sidekiq-scheduler)
  • Tailwind CSS v4, esbuild, Stimulus, Leaflet, Chart.js
  • ViewComponent (sidecar)

Local setup

rvm use 4.0.4
nvm use          # Node 20+ required (see .nvmrc / .node-version)
bundle install
yarn install
createdb waterlevels_development waterlevels_test
bin/rails db:migrate
cp .env.example .env   # add USGS_API_KEY
bin/dev

Bootstrap USGS data (rate-limit aware; prints progress). Catalog sync keeps active continuous water-body sites only (streams/lakes/estuaries with current latest-continuous data — not the full USGS well archive).

Production (preferred)

With the Sidekiq worker running, enqueue staggered per-state catalog+latest jobs:

bin/rails usgs:enqueue_bootstrap
# optional: STATE=wa DELAY_SECONDS=120

Hourly LatestObservationSyncJob keeps readings fresh. Hourly FloodStageSyncJob refreshes NWS NWPS flood categories from the national gauge list (by LID), prioritizes linking any currently flooding unlinked gauges, and discovers stage thresholds via USGS site-number detail lookups (STATE=wa bin/rails nwps:sync_flood_stages, or bin/rails nwps:enqueue_sync for staggered per-state jobs). Bootstrap also runs flood sync per state. Hourly HistoryBackfillBatchJob fills gap-aware continuous history (up to ~35 days) and year daily history into R2 in batches (gauge page views also enqueue a station when charts are empty). Prefer this over a national one-off usgs:bootstrap on a small dyno.

Local / single-state

STATE=wa bin/rails usgs:purge ALL=1   # wipe a bad/partial import
STATE=wa bin/rails usgs:bootstrap

Optional history backfill after bootstrap:

STATE=wa RANGE=1y LIMIT=25 bin/rails usgs:backfill
# optional deep daily fill after year history exists:
STATE=wa RANGE=3y LIMIT=25 bin/rails usgs:backfill

Tunables: USGS_REQUEST_PAUSE_MS (default 100 outside test), HISTORY_BACKFILL_BATCH (default 50 stations per cron tick for cold 1y work), HISTORY_DEEP_BACKFILL_BATCH (default 400 stations for 3y deep fills; set 0 to pause). History backfill pins one USGS key per purpose (USGS_API_HISTORY_CONTINUOUS_KEY / _DAILY_KEY / _PEAKS_KEY) and opens that purpose’s circuit on a 429 for the rest of the UTC hour. The batch runs Mon–Sat every 10 minutes and no-ops when all purpose circuits are open or the backfill queue is still draining. Circuit state per key is on /admin.

See doc/postgres-r2-daily-archive.md (current R2-first retention), doc/plan-3y-daily-history.md (historical 3y plan), and doc/future.md (hourly POR) for retention tiers and longer-history notes.

Local archive iteration (no Cloudflare): .env.example sets DAILY_ARCHIVE_STORE=local. After seeding or backfill, run bin/rails archive:export_daily — shards land in tmp/daily_archive and 1y / 3y charts read them when DAILY_ARCHIVE_READS=1.

Observability (Honeycomb)

OpenTelemetry traces export to Honeycomb when OTEL_EXPORTER_OTLP_* is set (see .env.example). ActiveRecord, PG, Redis, and Net::HTTP auto-spans are disabled to stay within event budgets; domain spans via Telemetry remain. Query recipes: doc/honeycomb-queries.md.

Tests

bin/rails test

Heroku

  • Dynos: web, worker (default queue + scheduler), sync_worker (sync queue), historical_worker (backfill queue)
  • Add-ons: Postgres, Redis
  • Set USGS_API_KEY (tip/catalog), optional USGS_API_HISTORY_CONTINUOUS_KEY / USGS_API_HISTORY_DAILY_KEY / USGS_API_HISTORY_PEAKS_KEY (purpose-pinned history backfill), REDIS_URL, DATABASE_URL, APP_HOST, SENTRY_DSN; optional CLOUDFLARE_ZONE_ID + CLOUDFLARE_API_TOKEN for post-sync Cache-Tag purge; optional CLOUDFLARE_R2_* for the yearly daily-means archive (doc/postgres-r2-daily-archive.md)
  • Enable runtime dyno metadata so HEROKU_RELEASE_VERSION is available; Sentry uses it as the release and tags environment as production
  • Open Graph PNGs are rendered with rsvg-convert (Aptfilelibrsvg2-bin). Requires heroku-community/apt as buildpack #1 (before Ruby) so the Aptfile packages install on the dyno.
  • Redis TLS: Sidekiq, cache, and Action Cable use ssl_params.verify_mode = VERIFY_NONE for Heroku self-signed rediss:// certs
  • After deploy: heroku run bin/rails usgs:enqueue_bootstrap -a <app>
  • Optional: MALLOC_ARENA_MAX=2 if worker RSS climbs
  • Put Cloudflare in front; honor Cache-Control / Cache-Tag from the app. Use a Cache Rule (Eligible for cache + Origin Cache Control) for public HTML; bypass /contact, /admin, and /api/*. Public pages skip the Rails session cookie so HTML is not forced to BYPASS.
  • Internal /api/* JSON is first-party-only (X-WaterLevels-Client: web + same-origin browser context), returns private, no-store, and is cached in Redis via ApiResponseCache (invalidated when syncs bump generation counters).
  • Optional ops dashboard at /admin when DASHBOARD_PW is set (session login at /admin/login). Returns 404 when the env var is unset. Login attempts are rate-limited (Rails rate_limit, 10 per 3 minutes per IP). Sidekiq Web is at /admin/sidekiq behind the same session.
  • Cold first request: Eco/Hobby web dynos sleep when idle; the next hit waits for Puma/Rails boot (often multi-second). Prefer an always-on web dyno, or ping /up every few minutes. Puma also warms DB/Redis/SiteStats on boot so a post-sleep origin render is cheaper once the process is up.

Contact form

GET /contact is served by PagesController (not edge-cached). POST /contact uses ContactMessage + invisible_captcha + Cloudflare Turnstile, then ContactMailer.

Set in .env:

  • TURNSTILE_SITE_KEY (defaults to the existing widget) / TURNSTILE_SECRET
  • CONTACT_TO_EMAIL / MAIL_FROM
  • BENTO_SITE_UUID, BENTO_PUBLISHABLE_KEY, BENTO_SECRET_KEY (Action Mailer via bento-actionmailer + premailer-rails)

Notes

  • Map may be empty until catalog sync lands locations.
  • Temperature is stored in °C; UI defaults to °F via a preference cookie.
  • Local PostGIS is optional; nearby stations use haversine precompute, map bbox uses lat/lon indexes.

About

WaterLevels.org

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages