P0 (security-critical): - Gate CSV/ADIF upload tabs behind authentication, add 30s cooldown to all upload handlers - Cap CSV/ADIF imports at 2,000 rows server-side in both parsers - Add submitter_verified boolean to contacts (client-cannot-set, anonymous=false) - Create k8s/secret.example.yaml with placeholders, add LIVE_VIEW_SIGNING_SALT P1 (high-priority): - Add Mox.verify_on_exit!() to valkey_test.exs - Replace DateTime.utc_now() truncation with static ~U literals in map_live_test.exs - Replace Process.sleep with render_async in pskr_spots_live_test.exs (6 occurrences) - Add MonitorLive.Show test coverage (4 tests: owner view, non-owner redirect, config success/error) - Extract duct-detection and mechanism-classification logic from ContactLive.Show into Propagation.PathAnalysis - Split ContactLive.Show render into 12 function components - Update CLAUDE.md: remove stale ML model, mark HRDPS active, add backtest/pskr dirs - Batch CSV import enrichment jobs via new enqueue_for_contacts/1 P2 (medium-priority): - Set secure:true on session and remember-me cookies in production - Change SMTP TLS from verify_none to verify_peer with public_key cacerts - Make /metrics fail-closed in production when PROMETHEUS_AUTH_TOKEN unset - Add RateLimiter (anon_limit:10, auth_limit:60) to /api/contacts/map - Add content-security-policy-report-only header - Add comment noting String.to_atom is compile-time safe in hrdps_client.ex - Delegate duplicated haversine_km to canonical Microwaveprop.Geo.haversine_km/4 - Consolidate score-tier/color/verdict formatting into Microwaveprop.Format - Update CLAUDE.md testing section to match actual raw-string-matching practice - Batch HrrrPointEnqueuer Repo.insert_all calls to single round-trip - Split weather.ex (1696→216 lines) and radio.ex (1285→54 lines) into purpose-based sub-facades P3 (low-priority): - Add LIVE_VIEW_SIGNING_SALT warning comment, extend filter_parameters - Add host/community validation to snmp_client.ex - Add raw/1 safety comment in algo_live.ex - Add hex-audit and cargo-audit Makefile targets - Add privacy_live smoke test - Replace notify_listener busy-poll loop with Process.monitor/1 + assert_receive - Add ContactCommonVolumeRadar changeset validation tests (5 tests)
187 lines
7.1 KiB
Elixir
187 lines
7.1 KiB
Elixir
# This file is responsible for configuring your application
|
||
# and its dependencies with the aid of the Config module.
|
||
#
|
||
# This configuration file is loaded before any dependency and
|
||
# is restricted to this project.
|
||
|
||
# General application configuration
|
||
import Config
|
||
|
||
alias Microwaveprop.Workers.CanadianSoundingFetchWorker
|
||
|
||
# Configure esbuild (the version is required)
|
||
config :esbuild,
|
||
version: "0.27.4",
|
||
microwaveprop: [
|
||
args:
|
||
~w(js/app.ts --bundle --target=es2022 --format=esm --splitting --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
|
||
cd: Path.expand("../assets", __DIR__),
|
||
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
|
||
]
|
||
|
||
config :live_table,
|
||
app: :microwaveprop,
|
||
repo: Microwaveprop.Repo,
|
||
pubsub: Microwaveprop.PubSub,
|
||
defaults: %{
|
||
custom_footer: {MicrowavepropWeb.LiveTableFooter, :render}
|
||
}
|
||
|
||
# Configure Elixir's Logger
|
||
config :logger, :default_formatter,
|
||
format: "$time $metadata[$level] $message\n",
|
||
metadata: [:request_id, :remote_ip, :method, :request_path]
|
||
|
||
# Configure the mailer
|
||
#
|
||
# By default it uses the "Local" adapter which stores the emails
|
||
# locally. You can see the emails in your browser, at "/dev/mailbox".
|
||
#
|
||
# For production it's recommended to configure a different adapter
|
||
# at the `config/runtime.exs`.
|
||
config :microwaveprop, Microwaveprop.Mailer, adapter: Swoosh.Adapters.Local
|
||
|
||
# PromEx metrics exporter. We surface the `/metrics` Prometheus scrape
|
||
# endpoint via MicrowavepropWeb.Router rather than PromEx's own HTTP
|
||
# server, so the router (and optional bearer-token auth in
|
||
# MicrowavepropWeb.MetricsPlug) stay the single ingress path. Grafana
|
||
# auto-provisioning is off — dashboards live in the dedicated Grafana
|
||
# instance and are managed separately.
|
||
config :microwaveprop, Microwaveprop.PromEx,
|
||
disabled: false,
|
||
manual_metrics_start_delay: :no_delay,
|
||
drop_metrics_groups: [],
|
||
grafana: :disabled,
|
||
metrics_server: :disabled
|
||
|
||
# Configure the endpoint
|
||
config :microwaveprop, MicrowavepropWeb.Endpoint,
|
||
url: [host: "localhost"],
|
||
adapter: Bandit.PhoenixAdapter,
|
||
render_errors: [
|
||
formats: [html: MicrowavepropWeb.ErrorHTML, json: MicrowavepropWeb.ErrorJSON],
|
||
layout: false
|
||
],
|
||
pubsub_server: Microwaveprop.PubSub
|
||
|
||
config :microwaveprop, Oban,
|
||
engine: Oban.Pro.Engines.Smart,
|
||
repo: Microwaveprop.Repo,
|
||
queues: [
|
||
solar: 1,
|
||
weather: 20,
|
||
enqueue: 1,
|
||
gefs: 2,
|
||
terrain: 4,
|
||
# DO NOT raise above 1 without measuring per-pod RSS first.
|
||
# RoverPathProfileWorker runs the full PathCompute pipeline
|
||
# (terrain sample analysis + 9 HRRR profile fetches/decodes +
|
||
# sounding readout + ionosphere readout + per-band scoring +
|
||
# loss + power + 18 h forecast). Per-job heap is roughly the
|
||
# same shape as the :propagation grid chain — ~500 MiB peak.
|
||
# With the deployment.yaml memory limit at 6 GiB and steady-state
|
||
# hot pods sitting at 3-5 GiB (ScoreCache + GridCache + LiveView
|
||
# state), even 2 concurrent path-computes can push a hot pod past
|
||
# the limit and trigger OOMKill. Verified on 2026-05-03: a
|
||
# backfill flood ran 3 slots/pod × 4 hot pods on the :terrain
|
||
# queue and OOMKilled two pods in a CrashLoopBackOff cycle.
|
||
# If you need more throughput, prefer adding hot replicas (HPA
|
||
# max is 4 in k8s/hpa.yaml) over raising this slot count, OR
|
||
# offload the HRRR profile fetch to hrrr-point-rs first.
|
||
rover_path: 1,
|
||
commercial: 2,
|
||
iemre: 10,
|
||
# 2 slots so PropagationPruneWorker can run alongside the
|
||
# forecast-hour chain job.
|
||
propagation: 2,
|
||
admin: 1,
|
||
nexrad: 2,
|
||
radar: 2,
|
||
ionosphere: 1,
|
||
space_weather: 1,
|
||
mechanism: 4,
|
||
contact_import: 4,
|
||
# HRDPS Canadian propagation chain seeder. Single slot — the worker
|
||
# only inserts grid_tasks rows, so concurrent execution buys nothing
|
||
# and uniqueness is enforced by the (run_time, kind, source) index.
|
||
# Dormant until the Rust prop-grid-rs HRDPS branch ships and the
|
||
# cron entry in runtime.exs is uncommented.
|
||
hrdps: 1
|
||
],
|
||
plugins: [
|
||
{Oban.Plugins.Pruner, max_age: 3600 * 24},
|
||
# See runtime.exs — producer-record-based orphan rescue, not
|
||
# timer-based. Fast recovery from rolling deploys.
|
||
{Oban.Pro.Plugins.DynamicLifeline, rescue_interval: to_timeout(second: 30)},
|
||
{Oban.Plugins.Cron,
|
||
crontab: [
|
||
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
|
||
{"*/5 * * * *", Microwaveprop.Commercial.PollWorker},
|
||
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker},
|
||
{"*/15 * * * *", Microwaveprop.Workers.PropagationPruneWorker},
|
||
{"15 * * * *", Microwaveprop.Workers.GridCachePruneWorker},
|
||
# Heals rover-planning missions whose paths are stuck in :pending
|
||
# or :failed (transient elevation-API errors, interrupted enqueues).
|
||
{"30 * * * *", Microwaveprop.Workers.RoverMissionBackfillWorker},
|
||
# UWYO publishes the 00Z/12Z Canadian radiosondes ~90 minutes after launch
|
||
{"30 1 * * *", CanadianSoundingFetchWorker},
|
||
{"30 13 * * *", CanadianSoundingFetchWorker},
|
||
# GIRO ionosonde stations publish at ~7.5 min cadence; poll every
|
||
# 10 min to pick up fresh foF2/foEs/hmF2 within one cycle.
|
||
{"*/10 * * * *", Microwaveprop.Workers.IonosphereFetchWorker},
|
||
# NOAA SWPC publishes Kp and GOES X-ray at 1-min cadence and F10.7
|
||
# hourly; poll every 5 min for reasonable freshness vs request volume.
|
||
{"*/5 * * * *", Microwaveprop.Workers.SpaceWeatherFetchWorker}
|
||
]}
|
||
]
|
||
|
||
# Per-band scoring weights file produced by `scripts/recalibrate.py`.
|
||
# Read once at app start by `Microwaveprop.Propagation.BandWeights` and
|
||
# cached in `:persistent_term`. Set to `false` to ignore the file and
|
||
# fall back to in-source `:weights` overrides + `@weights` defaults.
|
||
config :microwaveprop, :band_weights_json, "priv/algo/band_weights.json"
|
||
|
||
config :microwaveprop, :scopes,
|
||
user: [
|
||
default: true,
|
||
module: Microwaveprop.Accounts.Scope,
|
||
assign_key: :current_scope,
|
||
access_path: [:user, :id],
|
||
schema_key: :user_id,
|
||
schema_type: :binary_id,
|
||
schema_table: :users,
|
||
test_data_fixture: Microwaveprop.AccountsFixtures,
|
||
test_setup_helper: :register_and_log_in_user
|
||
]
|
||
|
||
config :microwaveprop,
|
||
ecto_repos: [Microwaveprop.Repo],
|
||
generators: [timestamp_type: :utc_datetime, binary_id: true]
|
||
|
||
# Log-filtered parameters — values matching these keys are scrubbed from
|
||
# request logs by Phoenix.Logger. Extends the default ~w(password)[a].
|
||
config :phoenix, :filter_parameters, ~w(password token api_key secret community authorization)
|
||
|
||
# Use EXLA as default Nx backend for accelerated tensor operations
|
||
if Mix.env() in [:dev, :test] do
|
||
config :nx, :default_backend, EXLA.Backend
|
||
end
|
||
|
||
# Use Jason for JSON parsing in Phoenix
|
||
config :phoenix, :json_library, Jason
|
||
|
||
# Configure tailwind (the version is required)
|
||
config :tailwind,
|
||
version: "4.2.2",
|
||
microwaveprop: [
|
||
args: ~w(
|
||
--input=assets/css/app.css
|
||
--output=priv/static/assets/css/app.css
|
||
),
|
||
|
||
# Import environment specific config. This must remain at the bottom
|
||
# of this file so it overrides the configuration defined above.
|
||
cd: Path.expand("..", __DIR__)
|
||
]
|
||
|
||
import_config "#{config_env()}.exs"
|