import Config # config/runtime.exs is executed for all environments, including # during releases. It is executed after compilation and before the # system starts, so it is typically used to load production configuration # and secrets from environment variables or elsewhere. Do not define # any compile-time configuration in here, as it won't be applied. # The block below contains prod specific runtime configuration. # ## Using releases # # If you use `mix release`, you need to explicitly enable the server # by passing the PHX_SERVER=true when you start it: # # PHX_SERVER=true bin/microwaveprop start # # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` # script that automatically sets the env var above. alias Microwaveprop.Workers.CanadianSoundingFetchWorker if System.get_env("PHX_SERVER") do config :microwaveprop, MicrowavepropWeb.Endpoint, server: true end config :microwaveprop, MicrowavepropWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))] if qrz_username = System.get_env("QRZ_USERNAME") do config :microwaveprop, Microwaveprop.Qrz.Client, username: qrz_username, password: System.get_env("QRZ_PASSWORD") || raise("QRZ_PASSWORD missing"), agent: System.get_env("QRZ_AGENT", "microwaveprop/1.0") end if google_api_key = System.get_env("GOOGLE_API_KEY") do config :microwaveprop, Microwaveprop.Geocoder, api_key: google_api_key end if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || raise """ environment variable DATABASE_URL is missing. For example: ecto://USER:PASS@HOST/DATABASE """ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you # want to use a different value for prod and you most likely don't want # to check this value into version control, so we use an environment # variable instead. secret_key_base = System.get_env("SECRET_KEY_BASE") || raise """ environment variable SECRET_KEY_BASE is missing. You can generate one by calling: mix phx.gen.secret """ host = System.get_env("PHX_HOST") || "example.com" # ## SSL Support # # To get SSL working, you will need to add the `https` key # to your endpoint configuration: # # config :microwaveprop, MicrowavepropWeb.Endpoint, # https: [ # ..., # port: 443, # cipher_suite: :strong, # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") # ] # # The `cipher_suite` is set to `:strong` to support only the # latest and more secure SSL ciphers. This means old browsers # and clients may not be supported. You can set it to # `:compatible` for wider support. # # `:keyfile` and `:certfile` expect an absolute path to the key # and cert in disk or a relative path inside priv, for example # "priv/ssl/server.key". For all supported SSL configuration # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 # # We also recommend setting `force_ssl` in your config/prod.exs, # ensuring no data is ever sent via http, always redirecting to https: # # config :microwaveprop, MicrowavepropWeb.Endpoint, # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. # ## Configuring the mailer # # In production you need to configure the mailer to use a different adapter. # Here is an example configuration for Mailgun: # # config :microwaveprop, Microwaveprop.Mailer, # adapter: Swoosh.Adapters.Mailgun, # tls: :always + explicit tls_options is required — SMTP2GO's endpoint sits # behind a wildcard cert (*.smtp2go.com) so SNI is mandatory, and gen_smtp # won't set it without explicit server_name_indication. Without this, the # handshake fails with an "unexpected_message" TLS alert. email_server = System.get_env("EMAIL_SERVER") config :libcluster, topologies: [ k8s: [ strategy: Cluster.Strategy.Kubernetes, config: [ mode: :ip, kubernetes_ip_lookup_mode: :pods, kubernetes_node_basename: "microwaveprop", kubernetes_selector: "app=prop", kubernetes_namespace: "prop" ] ] ] config :microwaveprop, Microwaveprop.Mailer, adapter: Swoosh.Adapters.SMTP, relay: email_server, port: 587, username: System.get_env("EMAIL_USERNAME"), password: System.get_env("EMAIL_PASSWORD"), tls: :always, tls_options: [ server_name_indication: String.to_charlist(email_server || ""), versions: [:"tlsv1.2", :"tlsv1.3"], verify: :verify_none ], auth: :always config :microwaveprop, Microwaveprop.Repo, # ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "20"), # For machines with several cores, consider starting multiple pools of `pool_size` # pool_count: 4, socket_options: maybe_ipv6 config :microwaveprop, MicrowavepropWeb.Endpoint, # SSL terminated by Cloudflare tunnel; generated URLs still use https url: [host: host, port: 443, scheme: "https"], http: [ # Enable IPv6 and bind on all interfaces. # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 # for details about using IPv6 vs IPv4 and loopback vs public addresses. ip: {0, 0, 0, 0, 0, 0, 0, 0} ], secret_key_base: secret_key_base # Production Oban: live scoring, polling, and on-demand QSO enrichment. # Runs on the Oban Pro Smart engine so we can use global_limit / rate_limit # to protect rate-limited upstreams (Copernicus CDS in particular). config :microwaveprop, Oban, engine: Oban.Pro.Engines.Smart, # Per-pod concurrency (×3 pods = effective cluster total) queues: [ # 2 slots so PropagationPruneWorker can run alongside the # forecast-hour chain job without blocking. propagation: 2, commercial: 1, solar: 1, weather: 3, hrrr: 1, terrain: 3, iemre: 3, # Historical backfill for pre-2014 contacts (pre-HRRR archive). # NARR is fetched anonymously from NCEI with no quota, no job # queue. Replaced the ERA5/CDS pipeline which never produced a # single row in prod. era5_cds_jobs table is kept for inspection # but its workers/client are gone. See # docs/plans/2026-04-15-merra2-historical-backfill.md. narr: 6, rtma: 2, backfill_enqueue: 1, admin: 1, nexrad: 2, radar: 2, ionosphere: 1, space_weather: 1, mechanism: 4, exports: 1, contact_import: 4 ], plugins: [ {Oban.Plugins.Pruner, max_age: 3600 * 24}, # DynamicLifeline uses producer records (heartbeats from each # live node) to rescue orphans, not a timer. When a pod dies # mid-deploy its Producer row disappears and any in-flight job # with that producer in `attempted_by` gets moved back to # `available` on the next rescue cycle — within ~30s instead of # waiting for a timer-based `rescue_after` to expire. Critical # for PropagationGridWorker chain steps to recover from rolling # deploys before the next hourly cron fire. {Oban.Pro.Plugins.DynamicLifeline, rescue_interval: to_timeout(second: 30)}, {Oban.Plugins.Cron, crontab: [ {"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker}, {"*/5 * * * *", Microwaveprop.Commercial.PollWorker}, # Hourly at :05. With scores written as binary files (not # Postgres) and HRRR grid profile persistence removed, a # full f00-f18 chain runs in ~45-60 min, so hourly fits. # If one chain slips past 60 min the :propagation queue's # concurrency-of-2 lets the next chain interleave; files # are keyed by (band, valid_time) so last-writer-wins gives # newer analysis data naturally. {"5 * * * *", Microwaveprop.Workers.PropagationGridWorker}, # MRMS refreshes the PrecipRate cache that AsosAdjustmentWorker # overlays onto the score grid. Both must run together — an # ASOS nudge without a fresh MRMS frame reverts to HRRR-only # rain. Requires wgrib2 built with PNG decoder support (DRT # 5.41), which the Dockerfile now enables via NCEPLIBS-g2c. {"*/2 * * * *", Microwaveprop.Workers.MrmsFetchWorker}, # AsosAdjustmentWorker disabled: depends on hrrr_profiles rows # that PropagationGridWorker no longer persists. The 10-min # ASOS nudge is a nice-to-have refinement we're not paying the # full grid-side JSONB write for. # {"*/10 * * * *", Microwaveprop.Workers.AsosAdjustmentWorker}, {"*/15 * * * *", Microwaveprop.Workers.PropagationPruneWorker}, # The :narr type is virtual: it targets contacts with hrrr_status = # :unavailable (pre-2014, missing from the HRRR archive) and dispatches # NarrFetchWorker against NCEI. See narr_jobs_for_contact/1. {"*/30 * * * *", Microwaveprop.Workers.BackfillEnqueueWorker, args: %{"types" => ["hrrr", "weather", "terrain", "iemre", "narr", "radar", "mechanism"]}}, # Hourly safety net for pos1/pos2/distance_km. Normally every contact # gets positions at insert time via Radio.resolve_grids_and_insert/1, # but direct DB writes (manual fixes, bulk imports) can bypass that. {"0 * * * *", Microwaveprop.Workers.ContactPositionBackfillWorker, args: %{"limit" => 500}}, # UWYO publishes the 00Z/12Z Canadian radiosondes ~90 minutes after launch {"30 1 * * *", CanadianSoundingFetchWorker}, {"30 13 * * *", CanadianSoundingFetchWorker}, # GIRO DIDBase publishes foF2/foEs/hmF2 at ~7.5 min cadence; poll # every 10 min per station (Millstone Hill, Alpena) for the 144 # MHz sporadic-E and HF MUF scoring inputs. {"*/10 * * * *", Microwaveprop.Workers.IonosphereFetchWorker}, # NOAA SWPC: Kp / GOES X-ray at 1-min cadence, F10.7 hourly. # 5-min poll balances freshness vs request volume. {"*/5 * * * *", Microwaveprop.Workers.SpaceWeatherFetchWorker} ]} ] config :microwaveprop, :email_from, {"NTMS Propagation", "prop@w5isp.com"} config :microwaveprop, hrrr_base_url: System.get_env("HRRR_BASE_URL", "http://skippy.w5isp.com:8080") config :microwaveprop, srtm_tiles_dir: "/data/srtm" # config :microwaveprop, start_freshness_monitor: true config :microwaveprop, start_freshness_monitor: false end